【问题标题】:Animating widget positions as the screen scrolls in flutter (GIF included)当屏幕在颤动中滚动时动画小部件位置(包括 GIF)
【发布时间】:2019-07-30 02:28:05
【问题描述】:

我正在尝试为两行小部件设置动画,以作为一个滚动折叠成这些小部件的 1 行。我试图在SliverAppBar 中实现这种行为。

为澄清起见,我在此处包含了一个 GIF 以供参考。我想要您在应用栏中看到的行为,但我希望 2 行变成 1,而不是 1 行到 2 行。

这是我目前所拥有的快速快照。我将 2 个包含 3 个 shrinkableBox 小部件的 Row 小部件包装成一个 Wrap 小部件。我通过连接_scrollController.offset 并进行一些计算来动态调整这些框的大小。这些行确实会动态移动,但它们不会设置动画并且会突然移动。

  double kExpandedHeight = 300.0;

  Widget build(BuildContext context) {
    double size = !_scrollController.hasClients || _scrollController.offset == 0 ? 75.0 : 75 - math.min(45.0, (45 / kExpandedHeight * math.min(_scrollController.offset, kExpandedHeight) * 1.5));
    return Scaffold(
      body: CustomScrollView(
          controller: _scrollController,
          slivers: <Widget>[           
            SliverAppBar(
              pinned: true,
              expandedHeight: kExpandedHeight,

          title: new Text(
            "Title!",
          ),
          bottom: PreferredSize(child: Wrap(
            children: <Widget>[
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  ShrinkableBox(
                    onClick: () {
                      print("tapped");
                    },
                    size: size,
                  ),
                  ShrinkableBox(
                    onClick: () {
                      print("tapped");
                    },
                    size: size,
                  ),                      
                  ShrinkableBox(
                    onClick: () {
                      print("tapped");
                    },
                    size: size,
                  ),
                Row(
                mainAxisAlignment: MainAxisAlignment.center,
                mainAxisSize: MainAxisSize.min,

                children: <Widget>[
                  ShrinkableBox(
                    onClick: () {
                      print("tapped");
                    },
                    size: size,
                  ),
                  ShrinkableBox(
                    onClick: () {
                      print("tapped");
                    },
                    size: size,
                  ),
                  ShrinkableBox(
                    onClick: () {
                      print("tapped");
                    },
                    size: size,
                  ),
                ],
              ),
            ],
          ), preferredSize: new Size.fromHeight(55),),
        )
   // ...
   // ...Other sliver list content here...
   // ...

【问题讨论】:

    标签: user-interface animation flutter


    【解决方案1】:

    您可以根据需要使用 Stack 和 Positioned 小部件来定位 ShrinkableBoxes。由于控制动画的是滚动偏移量,因此您不需要使用动画小部件或动画控制器或类似的东西。这是一个工作示例,它通过线性插值框的初始和最终位置来计算位置(您可以通过将 Curves.linear 更改为其他曲线来获得不同的动画路径):

    import 'dart:math' as math;
    import 'dart:ui';
    
    import 'package:flutter/material.dart';
    
    void main() {
      runApp(MaterialApp(home: Home()));
    }
    
    class Home extends StatefulWidget {
      @override
      State createState() => HomeState();
    }
    
    class HomeState extends State<Home> {
      static const double kExpandedHeight = 300.0;
    
      static const double kInitialSize = 75.0;
    
      static const double kFinalSize = 30.0;
    
      static const List<Color> kBoxColors = [
        Colors.red,
        Colors.green,
        Colors.yellow,
        Colors.purple,
        Colors.orange,
        Colors.grey,
      ];
    
      ScrollController _scrollController = new ScrollController();
    
      @override
      void initState() {
        _scrollController.addListener(() {
          setState(() { /* State being set is the Scroll Controller's offset */ });
        });
      }
    
      @override
      void dispose() {
        _scrollController.dispose();
      }
    
      Widget build(BuildContext context) {
        double size = !_scrollController.hasClients || _scrollController.offset == 0
            ? 75.0
            : 75 -
                math.min(45.0,
                    (45 / kExpandedHeight * math.min(_scrollController.offset, kExpandedHeight) * 1.5));
    
        return Scaffold(
          body: CustomScrollView(
            controller: _scrollController,
            slivers: <Widget>[
              SliverAppBar(
                pinned: true,
                expandedHeight: kExpandedHeight,
                title: Text("Title!"),
                bottom: PreferredSize(
                  preferredSize: Size.fromHeight(55),
                  child: buildAppBarBottom(size),
                ),
              ),
              SliverFixedExtentList(
                itemExtent: 50.0,
                delegate: SliverChildBuilderDelegate(
                  (BuildContext context, int index) {
                    return ListTile(title: Text('Item $index'));
                  },
                ),
              ),
            ],
          ),
        );
      }
    
      Widget buildAppBarBottom(double size) {
        double t = (size - kInitialSize) / (kFinalSize - kInitialSize);
    
        const double initialContainerHeight = 2 * kInitialSize;
        const double finalContainerHeight = kFinalSize;
    
        return Container(
          height: lerpDouble(initialContainerHeight, finalContainerHeight, t),
          child: LayoutBuilder(
            builder: (context, constraints) {
              List<Widget> stackChildren = [];
              for (int i = 0; i < 6; i++) {
                Offset offset = getInterpolatedOffset(i, constraints, t);
                stackChildren.add(Positioned(
                  left: offset.dx,
                  top: offset.dy,
                  child: buildSizedBox(size, kBoxColors[i]),
                ));
              }
    
              return Stack(children: stackChildren);
            },
          ),
        );
      }
    
      Offset getInterpolatedOffset(int index, BoxConstraints constraints, double t) {
        Curve curve = Curves.linear;
        double curveT = curve.transform(t);
    
        Offset a = getOffset(index, constraints, kInitialSize, 3);
        Offset b = getOffset(index, constraints, kFinalSize, 6);
    
        return Offset(
          lerpDouble(a.dx, b.dx, curveT),
          lerpDouble(a.dy, b.dy, curveT),
        );
      }
    
      Offset getOffset(int index, BoxConstraints constraints, double size, int columns) {
        int x = index % columns;
        int y = index ~/ columns;
        double horizontalMargin = (constraints.maxWidth - size * columns) / 2;
    
        return Offset(horizontalMargin + x * size, y * size);
      }
    
      Widget buildSizedBox(double size, Color color) {
        return Container(
          height: size,
          width: size,
          color: color,
        );
      }
    }
    

    【讨论】:

    • 这正是我想要的!你是怎么想出来的?这是一段相当复杂的代码。我也想为自己学习这样的动画。
    • @smbl 谢谢!我想我在 Flutter 中与动画有过相当多的斗争,呵呵。你查看过 Flutter 网站的Animations section 吗?虽然它解释了如何制作更复杂的动画,但您可能还想查看隐式动画小部件,它们仍然功能强大且易于使用,例如 AnimatedContainer:flutter.dev/docs/development/ui/widgets/animation
    • 对于带有弹跳滚动效果的ios,会抛出异常。
    • 您现在可以为堆栈子级使用新的 for 语法
    猜你喜欢
    • 2019-12-26
    • 2018-12-02
    • 2023-03-06
    • 2020-08-02
    • 2021-10-16
    • 2021-08-30
    • 1970-01-01
    • 2021-07-05
    • 2021-05-20
    相关资源
    最近更新 更多