【问题标题】:How to animate the position of the items in a SliverAppBar to move them around the title when closed如何在 SliverAppBar 中为项目的位置设置动画以在关闭时围绕标题移动它们
【发布时间】:2021-06-23 18:43:45
【问题描述】:

我对 Appbar 有这些要求,但我找不到解决这些问题的方法。

  • 拉伸时,AppBar 必须显示两个图像一个在另一个之上,并且标题必须隐藏。
  • 关闭时,AppBar 必须显示标题,并且两个图像在滚动时必须按比例缩小并移动到标题的两侧。滚动时标题可见。

我创建了几个模型来帮助获得所需的结果。

这是拉伸时的 Appbar:

这是关闭时的应用栏:

【问题讨论】:

    标签: flutter flutter-animation flutter-sliver sliverappbar flutter-sliverappbar


    【解决方案1】:

    您可以通过扩展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;
    }
    

    【讨论】:

    • 好一个!它已经足够接近我想要的了,但是我想要盒子,当 appbar 被拉伸时,就在靠近标题的地方,而不是在角落里。无论标题有多长。
    • 对不起,我不太明白。 “当 appbar 被拉伸时”是指在标题可见期间? “刚刚接近标题”的意思是shrinkedLeftPosshrinkedRightPos应该计算?
    • 对不起,我的错,我在手机上写得很快。我的意思是“缩小”,而不是“拉伸”。我想说的是,当 AppBar 在 minExtent 中时,最终布局必须是(伪代码)行(对齐:中​​心,孩子:[box,little padding,title,little padding,box])。但在您的解决方案中,框将被放置在应用栏的角落。
    • OK2.明白了。我稍后会修改答案。 (晚餐先:D)
    • @Mou 答案已更新。请参考shrinkedHorizontalPos
    【解决方案2】:

    它的公式有点乱,但这里是关于动画的所有计算的方法:

    UPD:添加到代码变量中以使图像在扩展时产生 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 但元素是由滚动插入的,随时计算
    • 是的,你是对的,但我们可以避免使用 AnimatedContainer,因为插值已经由滚动生成。另外,你还有一个固定的标题长度:)
    • 是的,你对容器的看法是正确的,修复了它,关于标题的保留原样。可能有人会像那样需要它。这个要求不在问题之列。
    猜你喜欢
    • 2021-10-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-06
    • 1970-01-01
    • 1970-01-01
    • 2016-11-01
    相关资源
    最近更新 更多