如何使SliverAppBar中的项目位置动画化,以在关闭时将它们围绕标题移动。

8

我有以下对于应用栏的需求,但是我找不到解决方法。

  • 当展开时,应用栏需要同时显示两个图像并以层叠形式呈现,标题需要隐藏。
  • 当收缩时,应用栏需要显示标题,并且两个图像在滚动时变小并移动到标题两侧。当滚动时,标题将变为可见状态。

我创建了一些演示图片以帮助达到所需的结果。

这是展开时的应用栏:

这里是图片描述

这是收缩时的应用栏:

这里是图片描述

2个回答

5

您可以通过扩展 SliverPersistentHeaderDelegate 来创建自己的 SliverAppBar

翻译、缩放和不透明度的更改将在 build(...) 方法中完成,因为这将在范围更改期间(通过滚动)调用,即 minExtent <-> maxExtent

下面是一个示例代码:

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: Colors.blue,
      ),
      home: HomePage(),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: CustomScrollView(
        slivers: <Widget>[
          SliverPersistentHeader(
            delegate: MySliverAppBar(
              title: 'Sample',
              minWidth: 50,
              minHeight: 25,
              leftMaxWidth: 200,
              leftMaxHeight: 100,
              rightMaxWidth: 100,
              rightMaxHeight: 50,
              shrinkedTopPos: 10,
            ),
            pinned: true,
          ),
          SliverList(
            delegate: SliverChildBuilderDelegate(
              (_, int i) => Container(
                height: 50,
                color: Color.fromARGB(
                  255,
                  Random().nextInt(255),
                  Random().nextInt(255),
                  Random().nextInt(255),
                ),
              ),
              childCount: 50,
            ),
          ),
        ],
      ),
    );
  }
}

class MySliverAppBar extends SliverPersistentHeaderDelegate {
  MySliverAppBar({
    required this.title,
    required this.minWidth,
    required this.minHeight,
    required this.leftMaxWidth,
    required this.leftMaxHeight,
    required this.rightMaxWidth,
    required this.rightMaxHeight,
    this.titleStyle = const TextStyle(fontSize: 26),
    this.shrinkedTopPos = 0,
  });

  final String title;
  final TextStyle titleStyle;
  final double minWidth;
  final double minHeight;
  final double leftMaxWidth;
  final double leftMaxHeight;
  final double rightMaxWidth;
  final double rightMaxHeight;

  final double shrinkedTopPos;

  final GlobalKey _titleKey = GlobalKey();

  double? _topPadding;
  double? _centerX;
  Size? _titleSize;

  double get _shrinkedTopPos => _topPadding! + shrinkedTopPos;

  @override
  Widget build(
    BuildContext context,
    double shrinkOffset,
    bool overlapsContent,
  ) {
    if (_topPadding == null) {
      _topPadding = MediaQuery.of(context).padding.top;
    }
    if (_centerX == null) {
      _centerX = MediaQuery.of(context).size.width / 2;
    }
    if (_titleSize == null) {
      _titleSize = _calculateTitleSize(title, titleStyle);
    }

    double percent = shrinkOffset / (maxExtent - minExtent);
    percent = percent > 1 ? 1 : percent;

    return Container(
      color: Colors.red,
      child: Stack(
        children: <Widget>[
          _buildTitle(shrinkOffset),
          _buildLeftImage(percent),
          _buildRightImage(percent),
        ],
      ),
    );
  }

  Size _calculateTitleSize(String text, TextStyle style) {
    final TextPainter textPainter = TextPainter(
        text: TextSpan(text: text, style: style),
        maxLines: 1,
        textDirection: TextDirection.ltr)
      ..layout(minWidth: 0, maxWidth: double.infinity);
    return textPainter.size;
  }

  Widget _buildTitle(double shrinkOffset) => Align(
        alignment: Alignment.topCenter,
        child: Padding(
          padding: EdgeInsets.only(top: _topPadding!),
          child: Opacity(
            opacity: shrinkOffset / maxExtent,
            child: Text(title, key: _titleKey, style: titleStyle),
          ),
        ),
      );

  double getScaledWidth(double width, double percent) =>
      width - ((width - minWidth) * percent);

  double getScaledHeight(double height, double percent) =>
      height - ((height - minHeight) * percent);

  /// 20 is the padding between the image and the title
  double get shrinkedHorizontalPos =>
      (_centerX! - (_titleSize!.width / 2)) - minWidth - 20;

  Widget _buildLeftImage(double percent) {
    final double topMargin = minExtent;
    final double rangeLeft =
        (_centerX! - (leftMaxWidth / 2)) - shrinkedHorizontalPos;
    final double rangeTop = topMargin - _shrinkedTopPos;

    final double top = topMargin - (rangeTop * percent);
    final double left =
        (_centerX! - (leftMaxWidth / 2)) - (rangeLeft * percent);

    return Positioned(
      left: left,
      top: top,
      child: Container(
        width: getScaledWidth(leftMaxWidth, percent),
        height: getScaledHeight(leftMaxHeight, percent),
        color: Colors.black,
      ),
    );
  }

  Widget _buildRightImage(double percent) {
    final double topMargin = minExtent + (rightMaxHeight / 2);
    final double rangeRight =
        (_centerX! - (rightMaxWidth / 2)) - shrinkedHorizontalPos;
    final double rangeTop = topMargin - _shrinkedTopPos;

    final double top = topMargin - (rangeTop * percent);
    final double right =
        (_centerX! - (rightMaxWidth / 2)) - (rangeRight * percent);

    return Positioned(
      right: right,
      top: top,
      child: Container(
        width: getScaledWidth(rightMaxWidth, percent),
        height: getScaledHeight(rightMaxHeight, percent),
        color: Colors.white,
      ),
    );
  }

  @override
  double get maxExtent => 300;

  @override
  double get minExtent => _topPadding! + 50;

  @override
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
      false;
}

不错!这已经很接近我想要的了,但是当应用栏被拉伸时,我希望盒子紧贴标题,而不是在角落里。无论标题有多长。 - Mou
抱歉,我不太理解。“当应用栏被拉伸时”是指标题可见时吗?“靠近标题”意味着应计算shrinkedLeftPosshrinkedRightPos吗? - rickimaru
抱歉,我的错,我在手机上写得太快了。我想说的是“缩小”,而不是“拉伸”。我的意思是最终布局,在AppBar处于minExtent时,必须是一种(伪代码)Row(align:center,children:[box,little padding,title,little padding,box])。但在您的解决方案中,盒子将放置在应用栏的角落里。 - Mou
@Mou 答案已更新。请参考 shrinkedHorizontalPos - rickimaru
@rickimaru 很棒的文本大小功能!你有我的投票! - Simon Sot
显示剩余4条评论

2

这里有一些关于动画的计算公式,可能有点复杂,但是以下是如何进行所有计算的方法:

更新: 添加了变量代码,以使得在扩展图像时Y轴偏移。

完整的可复制代码:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      home: Body(),
    );
  }
}

class Body extends StatefulWidget {
  const Body({
    Key key,
  }) : super(key: key);

  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> {
  double _collapsedHeight = 60;
  double _expandedHeight = 200;
  double
      extentRatio; // Value to control SliverAppBar widget sizes, based on BoxConstraints and
  double minH1 = 40; // Minimum height of the first image.
  double minW1 = 30; // Minimum width of the first image.
  double minH2 = 20; // Minimum height of second image.
  double minW2 = 25; // Minimum width of second image.
  double maxH1 = 60; // Maximum height of the first image.
  double maxW1 = 60; // Maximum width of the first image.
  double maxH2 = 40; // Maximum height of second image.
  double maxW2 = 50; // Maximum width of second image.
  double textWidth = 70; // Width of a given title text.
  double extYAxisOff = 10.0; // Offset on Y axis for both images when sliver is extended.
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              SliverAppBar(
                  collapsedHeight: _collapsedHeight,
                  expandedHeight: _expandedHeight,
                  floating: true,
                  pinned: true,
                  flexibleSpace: LayoutBuilder(
                    builder:
                        (BuildContext context, BoxConstraints constraints) {
                      extentRatio =
                          (constraints.biggest.height - _collapsedHeight) /
                              (_expandedHeight - _collapsedHeight);
                      double xAxisOffset1 = (-(minW1 - minW2) -
                              textWidth +
                              (textWidth + maxW1) * extentRatio) /
                          2;
                      double xAxisOffset2 = (-(minW1 - minW2) +
                              textWidth +
                              (-textWidth - maxW2) * extentRatio) /
                          2;
                      double yAxisOffset2 = (-(minH1 - minH2) -
                                  (maxH1 - maxH2 - (minH1 - minH2)) *
                                      extentRatio) /
                              2 -
                          extYAxisOff * extentRatio;
                      double yAxisOffset1 = -extYAxisOff * extentRatio;
                      print(extYAxisOff);
                      // debugPrint('constraints=' + constraints.toString());
                      // debugPrint('Scale ratio is $extentRatio');
                      return FlexibleSpaceBar(
                        titlePadding: EdgeInsets.all(0),
                        // centerTitle: true,
                        title: Stack(
                          children: [
                            Align(
                              alignment: Alignment.topCenter,
                              child: AnimatedOpacity(
                                duration: Duration(milliseconds: 300),
                                opacity: extentRatio < 1 ? 1 : 0,
                                child: Padding(
                                  padding: const EdgeInsets.only(top: 30.0),
                                  child: Container(
                                    color: Colors.indigo,
                                    width: textWidth,
                                    alignment: Alignment.center,
                                    height: 20,
                                    child: Text(
                                      "TITLE TEXT",
                                      style: TextStyle(
                                        color: Colors.white,
                                        fontSize: 12.0,
                                      ),
                                    ),
                                  ),
                                ),
                              ),
                            ),
                            Align(
                              alignment: Alignment.bottomCenter,
                              child: Row(
                                crossAxisAlignment: CrossAxisAlignment.end,
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  Container(
                                    transform: Matrix4(
                                        1,0,0,0,
                                        0,1,0,0,
                                        0,0,1,0,
                                        xAxisOffset1,yAxisOffset1,0,1),
                                    width:
                                        minW1 + (maxW1 - minW1) * extentRatio,
                                    height:
                                        minH1 + (maxH1 - minH1) * extentRatio,
                                    color: Colors.red,
                                  ),
                                  Container(
                                    transform: Matrix4(
                                        1,0,0,0,
                                        0,1,0,0,
                                        0,0,1,0,
                                        xAxisOffset2,yAxisOffset2,0,1),
                                  
                                    width:
                                        minW2 + (maxW2 - minW2) * extentRatio,
                                    height:
                                        minH2 + (maxH2 - minH2) * extentRatio,
                                    color: Colors.purple,
                                  ),
                                ],
                              ),
                            ),
                          ],
                        ),
                      );
                    },
                  )),
            ];
          },
          body: Center(
            child: Text("Sample Text"),
          ),
        ),
      ),
    );
  }
}


看起来不错!但是标题长度可能会变化,不能使用AnimatedContainer,因为元素必须由滚动本身插值,而不是由“外部”动画插值。 - Mou
@Mou 但元素是通过滚动插值计算的。 - Simon Sot
是的,你说得对,但我们可以避免使用AnimatedContainer,因为插值已经由滚动生成。此外,您仍然有一个固定的标题长度 :) - Mou
是的,你关于容器的想法是正确的,我已经修复了。至于标题,我保留原样。也许有人会需要它。那个要求并没有在问题中提到。 - Simon Sot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接