前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter开发实战分析-animation_demo瞎复写总结

Flutter开发实战分析-animation_demo瞎复写总结

作者头像
deep_sadness
发布2018-08-30 10:53:24
2.5K0
发布2018-08-30 10:53:24
举报
文章被收录于专栏:Flutter入门Flutter入门

以下代码基本参考于 flutter_gallery中的animation_demo示例。(可以结合本文看源码)

题外话:这个demo是最炫酷的了

animation.gif

这里的动画效果我们看到:

  1. 有一个多页的滚动
  2. 滑到上下滑到将近一半,会有一个粘性效果,吸附到一半。再往上,就正常滑动。 3.一半往上,下面的白色标签开始发生位移。一半往下,整个4个卡片发生位移。

简单的分析一下

  1. 上下滚动,并且自定义动画效果。嗯。上一遍文章的CustomScrollView
  2. 左右滚动,切换页面。嗯。PageView。 PageView可以让像是一页一页滑动。而且每个页面的大小是一样的。 使用PageController 来进行控制。
  3. 上下要同时切换。肯定也需要上下两个PageView的状态同步。

第一次接触

  • 先准备好数据。查看sections.dart。可以不管,先复制过来。
  • 初始化布局。 像是大体想象的框架应该是CustomScrollView.然后初始的SliveAppBar的高度应该是屏幕的高度。SliveAppBar的child是PageView 下面是一个SliveToBoxAdapter里面也放着PageView.
  • 代码 按照我们初步的想法,代码如下
代码语言:javascript
复制
import 'package:flutter/material.dart';
import 'package:flutter_start/demo/animation/sections.dart';

Color _kAppBackgroundColor = const Color(0xFF353662);
Duration _kScrollDuration = const Duration(milliseconds: 400);
Curve _kScrollCurve = Curves.fastOutSlowIn;

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

  static const String routeName = '/animation';

  @override
  _AnimationDemoHomeState createState() => new _AnimationDemoHomeState();
}

class _AnimationDemoHomeState extends State<AnimationDemoHome> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      backgroundColor: _kAppBackgroundColor,
      body: new Builder(
        // Insert an element so that _buildBody can find the PrimaryScrollController.
        builder: _buildBody,
      ),
    );
  }

  Widget _buildBody(BuildContext context) {
    double height = MediaQuery.of(context).size.height;
    return new SizedBox.expand(
      child: new Stack(
        children: <Widget>[
          new CustomScrollView(
            slivers: <Widget>[
              SliverAppBar(
                expandedHeight: height,
                flexibleSpace: LayoutBuilder(builder:
                    (BuildContext context, BoxConstraints constraints) {
                  return PageView(
                    children: allSections.map((Section section) {
                      return _headerItemsFor(section);
                    }).toList(),
                  );
                }),
              ),
              SliverToBoxAdapter(
                child: SizedBox(
                  height: 610.0,
                  child: PageView(
                    children: allSections.map((Section section) {
                      return Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: _detailItemsFor(section).toList(),
                      );
                    }).toList(),
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Iterable<Widget> _detailItemsFor(Section section) {
    final Iterable<Widget> detailItems =
        section.details.map((SectionDetail detail) {
      return new SectionDetailView(detail: detail);
    });
    return ListTile.divideTiles(context: context, tiles: detailItems);
  }

  Widget _headerItemsFor(Section section) {
    return SectionCard(section: section);
  }
}

class SectionDetailView extends StatelessWidget {
  SectionDetailView({Key key, @required this.detail})
      : assert(detail != null && detail.imageAsset != null),
        assert((detail.imageAsset ?? detail.title) != null),
        super(key: key);

  final SectionDetail detail;

  @override
  Widget build(BuildContext context) {
    final Widget image = new DecoratedBox(
      decoration: new BoxDecoration(
        borderRadius: new BorderRadius.circular(6.0),
        image: new DecorationImage(
          image: new AssetImage(
            detail.imageAsset,
            package: detail.imageAssetPackage,
          ),
          fit: BoxFit.cover,
          alignment: Alignment.center,
        ),
      ),
    );

    Widget item;
    if (detail.title == null && detail.subtitle == null) {
      item = new Container(
        height: 240.0,
        padding: const EdgeInsets.all(16.0),
        child: new SafeArea(
          top: false,
          bottom: false,
          child: image,
        ),
      );
    } else {
      item = new ListTile(
        title: new Text(detail.title),
        subtitle: new Text(detail.subtitle),
        leading: new SizedBox(width: 32.0, height: 32.0, child: image),
      );
    }

    return new DecoratedBox(
      decoration: new BoxDecoration(color: Colors.grey.shade200),
      child: item,
    );
  }
}

class SectionCard extends StatelessWidget {
  const SectionCard({Key key, @required this.section})
      : assert(section != null),
        super(key: key);

  final Section section;

  @override
  Widget build(BuildContext context) {
    return new Semantics(
      label: section.title,
      button: true,
      child: new DecoratedBox(
        decoration: new BoxDecoration(
          gradient: new LinearGradient(
            begin: Alignment.centerLeft,
            end: Alignment.centerRight,
            colors: <Color>[
              section.leftColor,
              section.rightColor,
            ],
          ),
        ),
        child: new Image.asset(
          section.backgroundAsset,
          package: section.backgroundAssetPackage,
          color: const Color.fromRGBO(255, 255, 255, 0.075),
          colorBlendMode: BlendMode.modulate,
          fit: BoxFit.cover,
        ),
      ),
    );
  }
}
  • 效果

target-20180814110704.gif

发现我们的想法还是有一定偏差的。上面的头部部分,不只是pageView,它需要从一个list然后移动变成pageView.

CustomMultiChildLayout

这个Widget可以完全自己掌控布局的排列。我们需要做的是将它的自组件都传递给他,然后实现它的方法,就可以完全的掌握自己的布局了。 使用它有两个关键点:

  1. 自定义MultiChildLayoutDelegate来自己实现布局
  2. 他的每个child都需要用layoutId来包裹,并且分配给他们的id,都必须是唯一的。

按照这个思路,我们希望每一个Page都是能实现这个样的动画效果,所以我们自己定义CustomMultiChildLayout作为PageView的child。 同时,我们还需要将之前的4个SectionsCard用LayoutId包裹后,传入其中。

  • 自定义实现MultiChildLayoutDelegate
代码语言:javascript
复制
class _AllSectionsLayout extends MultiChildLayoutDelegate {
  int cardCount = 4;
  double selectedIndex = 0.0;
  double tColumnToRow = 0.0;
 ///Alignment(-1.0, -1.0) 表示矩形的左上角。
  ///Alignment(1.0, 1.0) 代表矩形的右下角。
  Alignment translation = new Alignment(0 * 2.0 - 1.0, -1.0);
  _AllSectionsLayout({this.tColumnToRow,this.selectedIndex,this.translation});

  @override
  void performLayout(Size size) {
    //初始值
    //竖向布局时
    //卡片的left
    final double columnCardX = size.width / 5.0;
    //卡片的宽度Width
    final double columnCardWidth = size.width - columnCardX;
    //卡片的高度
    final double columnCardHeight = size.height / cardCount;
    //横向布局时
    final double rowCardWidth = size.width;

    final Offset offset = translation.alongSize(size);

    double columnCardY = 0.0;
    double rowCardX = -(selectedIndex * rowCardWidth);

    for (int index = 0; index < cardCount; index++) {
      // Layout the card for index.
      final Rect columnCardRect = new Rect.fromLTWH(
          columnCardX, columnCardY, columnCardWidth, columnCardHeight);
      final Rect rowCardRect =
          new Rect.fromLTWH(rowCardX, 0.0, rowCardWidth, size.height);
      //  定义好初始的位置和结束的位置,就可以使用这个lerp函数,轻松的找到中间状态值
      //rect 的 shift ,相当于 offset的translate 
      final Rect cardRect =
          _interpolateRect(columnCardRect, rowCardRect).shift(offset);
      final String cardId = 'card$index';
      if (hasChild(cardId)) {
        layoutChild(cardId, new BoxConstraints.tight(cardRect.size));
        positionChild(cardId, cardRect.topLeft);
      }

      columnCardY += columnCardHeight;
      rowCardX += rowCardWidth;
    }
  }

  @override
  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) {
    print('oldDelegate=$oldDelegate');
    return false;
  }

  Rect _interpolateRect(Rect begin, Rect end) {
    return Rect.lerp(begin, end, tColumnToRow);
  }

  Offset _interpolatePoint(Offset begin, Offset end) {
    return Offset.lerp(begin, end, tColumnToRow);
  }
}
定义整个动画过程

整个动画效果就是,从竖排的4列,变化成横排的4列。 为每个card定义好

动画的初始

card的初始状态column为前缀的变量。

  • 高度 就是按照我们看到的,竖排的情况下,每个Card的高度是整个appBar高度的4分之一。
  • left 统一的位置。
  • 宽度 去掉left部分的,宽度
  • Offset Offset需要确定的位置,需要和选定的坐标协同。选定的Index,毕竟出现在当前位置。就是他的Offset的x,必须和自己的left相反,这样才能在第一个位置。 它是用Aligment.alongSize来进行转换。Alignment(-1.0, -1.0)就代表左上角。Alignment(1.0, 1.0)代表矩形的右下角。整个Aligment相当于一个边长为2,中心点在原点的正方形。 需要让index== selectedIndex的card的Aligment为左上角Alignment(1.0, 1.0)的状态。然后其他对应的进行偏移。
动画的结尾

card的最终状态row为前缀的变量

  • 高度 就是整个的高度
  • left 就是选中card的偏移量。
  • 宽度 就是整个的宽度
  • offset 同上。
确定中间状态
  • tColumnToRow 整体的动画,在Flutter中有很方便的lerp函数可以确定中间的状态。只要传入我们进度的百分比就可以。这个百分比可以由滑动的过程中的offset传入。

SliverAppBar

代码语言:javascript
复制
//只显示sliverAppBar部分
 slivers: <Widget>[
            NotificationListener<ScrollNotification>(
              onNotification: (ScrollNotification notification) {
                return _handleScrollNotification(
                    notification, appBarMidScrollOffset);
              },
              child: SliverAppBar(
                backgroundColor: _kAppBackgroundColor,
                expandedHeight: height - statusHeight,
                bottom: PreferredSize(
                  preferredSize:
                      const Size.fromHeight(_kAppBarMinHeight - kToolbarHeight),
                  child: Container(width: 0.0, height: 0.0),
                ),
                pinned: true,
                    //同样根据上一节我们学习到的内容,我们可以通过layoutbuilder来获取变化的约束
                flexibleSpace: LayoutBuilder(builder:
                    (BuildContext context, BoxConstraints constraints) {
                //因为发现当滚动成column时,上面有statusBar高度的padding,当变成row时,整个padding就变成0,所以这里是这个的变化值
                  double t =
                      1.0 - (height - constraints.maxHeight) / (height * 0.3);
                  final Curve statusBarHeightCurve =
                      Interval(0.0, 1.0, curve: Curves.fastOutSlowIn);
                  double extraPaddingTop =
                      statusHeight * statusBarHeightCurve.transform(t.clamp(0.0, 1.0));
                  
                  //这里开始计算 tColumnToRow的比例。其实就是滚动的距离。
                  final Size size = constraints.biggest;
                  final double tColumnToRow = 1.0 -
                      ((size.height - _kAppBarMidHeight) /
                              (height - statusHeight - _kAppBarMidHeight))
                          .clamp(0.0, 1.0);

                  final List<Widget> sectionCards = <Widget>[];

                  for (int index = 0; index < allSections.length; index++) {
                    Section section = allSections[index];
                    sectionCards.add(_headerItemsFor(section));
                  }
                  List<Widget> children = [];
                  for (int index = 0; index < sectionCards.length; index++) {
                    //这里一定要注意,   CustomMultiChildLayout中的,子节点,都必须用LayoutId来包裹!!!
                    children.add(new LayoutId(
                      id: 'card$index',
                      child: sectionCards[index],
                    ));
                  }

                  List<Widget> layoutChildren = [];

                  print('selectedIndex.value=${selectedIndex.value}');
                  for (int index = 0; index < sectionCards.length; index++) {
                    layoutChildren.add(new CustomMultiChildLayout(
                      delegate: _AllSectionsLayout(
                          tColumnToRow: tColumnToRow,
                          translation: new Alignment(
                              (selectedIndex.value - index) * 2.0 - 1.0, -1.0),
                          selectedIndex: selectedIndex.value
                          ),
                      children: children,
                    ));
                  }
                  //将上面的用PageView再包裹一次。
                  return Padding(
                      padding: EdgeInsets.only(top: extraPaddingTop),
                      child: PageView(
                        physics: _headingScrollPhysics,
                        controller: _headingPageController,
                        children: layoutChildren,
                      ),
                    ),
                  );
                }),
              ),
            ),

上面这段代码,有下面几个重点

  • SliverAppBar的bottom 因为我们使用Pinned属性。这个属性会悬浮我们的AppBar在顶部。但是如果默认情况下,这时appBar的高度就是有56逻辑像素这样。所以,我们需要添加一个bottom,让它,增加到我们想要的最后高度。
  • 调整整体的padding 从动画效果可以看到,padding有一个从有到无的状态,当从column变成row的过程中,所以我们要对其进行计算。
  • 计算tColumnToRow 这个值也是根据我们滑动的整体状态来计算的。
  • LayoutId 这个一定要记住! CustomMultiChildLayout中的,子节点,都必须用LayoutId来包裹!!!

然后,我还要处理两个细节。

一个是当滚动到中间位置后,就不能左右切换了。
  • 监听 将NotificationListener包裹在pageView之外,就可以监听PageView的滚动事件了。
代码语言:javascript
复制
//省略代码...
 NotificationListener<ScrollNotification>(
                    onNotification: (ScrollNotification notification) {
                      return _handlePageNotification(notification,
                          _headingPageController, _detailsPageController);
                    },
                    child: Padding(
                      padding: EdgeInsets.only(top: extraPaddingTop),
                      child: PageView(
                        physics: _headingScrollPhysics,
                        controller: _headingPageController,
                        children: layoutChildren,
                      ),
                    ),
                  );
  • 切换 这个需要监听,滚动的事件,当滚动的距离得到一般之后,就将PageView的physics改为NeverScrollableScrollPhysics。它将会导致页面不能滚动。 反之,就设置为PageScrollPhysics().像页面一样滚动。
代码语言:javascript
复制
 bool _handleScrollNotification(
      ScrollNotification notification, double midScrollOffset) {
    if (notification.depth == 0 && notification is ScrollUpdateNotification) {
      final ScrollPhysics physics =
          _scrollController.position.pixels >= midScrollOffset
              ? const PageScrollPhysics()
              : const NeverScrollableScrollPhysics();
      if (physics != _headingScrollPhysics) {
        setState(() {
          _headingScrollPhysics = physics;
        });
      }
    }
    return false;
  }
当快滚动中间位置时,会有一个粘性的效果

这个效果是整个SliverAppBar来提供的。所以设置他的physics。 当滚动的距离大于一办时,判断对应的滚动反向,来创造对应simulation

代码语言:javascript
复制
class _SnappingScrollPhysics extends ClampingScrollPhysics {
  const _SnappingScrollPhysics({
    ScrollPhysics parent,
    @required this.midScrollOffset,
  })  : assert(midScrollOffset != null),
        super(parent: parent);

  final double midScrollOffset;

  @override
  _SnappingScrollPhysics applyTo(ScrollPhysics ancestor) {
    return new _SnappingScrollPhysics(
        parent: buildParent(ancestor), midScrollOffset: midScrollOffset);
  }

  Simulation _toMidScrollOffsetSimulation(double offset, double dragVelocity) {
    final double velocity = math.max(dragVelocity, minFlingVelocity);
    return new ScrollSpringSimulation(spring, offset, midScrollOffset, velocity,
        tolerance: tolerance);
  }

  Simulation _toZeroScrollOffsetSimulation(double offset, double dragVelocity) {
    final double velocity = math.max(dragVelocity, minFlingVelocity);
    return new ScrollSpringSimulation(spring, offset, 0.0, velocity,
        tolerance: tolerance);
  }

  @override
  Simulation createBallisticSimulation(
      ScrollMetrics position, double dragVelocity) {
    final Simulation simulation =
        super.createBallisticSimulation(position, dragVelocity);
    final double offset = position.pixels;

    if (simulation != null) {
      // The drag ended with sufficient velocity to trigger creating a simulation.
      // If the simulation is headed up towards midScrollOffset but will not reach it,
      // then snap it there. Similarly if the simulation is headed down past
      // midScrollOffset but will not reach zero, then snap it to zero.
      final double simulationEnd = simulation.x(double.infinity);
      if (simulationEnd >= midScrollOffset) return simulation;
      if (dragVelocity > 0.0)
        return _toMidScrollOffsetSimulation(offset, dragVelocity);
      if (dragVelocity < 0.0)
        return _toZeroScrollOffsetSimulation(offset, dragVelocity);
    } else {
      // The user ended the drag with little or no velocity. If they
      // didn't leave the offset above midScrollOffset, then
      // snap to midScrollOffset if they're more than halfway there,
      // otherwise snap to zero.
      final double snapThreshold = midScrollOffset / 2.0;
      if (offset >= snapThreshold && offset < midScrollOffset)
        return _toMidScrollOffsetSimulation(offset, dragVelocity);
      if (offset > 0.0 && offset < snapThreshold)
        return _toZeroScrollOffsetSimulation(offset, dragVelocity);
    }
    return simulation;
  }
}

运行效果

target-20180814215213.gif

这样,我们就做成很接近最后效果的动画了。要实现最后的动画,只要用相同的办法去创建titleindicator就行了。

总结

虽然我们的代码,和animation_demo源码中的代码有所不一样。但是核心是一样的。 这边文章我们熟悉了

CustomScrollViewMultiChildLayoutDelegate

通过CustomScrollViewMultiChildLayoutDelegateperformLayout方法的实现,来完成自定义的多组件之间的布局。

自定义动画的过程

自定义动画的过程,在Flutter中其实相对简单。提供了很多帮助的计算方式。需要做的是确定要初始值,和最终值,中间的过度变量可以考虑使用lerp就可以完成。

监听事件

之前的文章,我们分析过Flutter中数据的传递。需要监听发送的ScrollEvent,我们只要在我们监听的Widget的外层,套一层NotificationListener进行监听就好

ScrollView的要素

我们更加熟悉了ScrollView的两个要素。controllerphysics

  • controller 我们可以得到滚动的状态,和控制滚动的情况。
  • physics 滚动的效果。我们可以添加NeverScrollableScrollPhysics。这样就不滚动了。添加PageScrolPhysics,这样就是按照页面滚动。添加BounceScrollPhysics,就实现ios中的弹性滚动了。

好的。这边文章,我们就暂时到这里。 下一遍文章,我们先介绍一个Flutter中整体的视图树,然后回顾一下我们遇到过的组件。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.08.14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 第一次接触
  • CustomMultiChildLayout
  • SliverAppBar
  • 总结
    • CustomScrollView的MultiChildLayoutDelegate
      • 自定义动画的过程
        • 监听事件
          • ScrollView的要素
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档