Flutter开发实战分析-animation_demo解析导读

以下代码基本参考于 flutter_gallery中的animation_demo示例。(可以结合本文看源码)
整体动画效果预览

animation.gif

顶部的statusBar部分的高度变化

target-20180816144749.gif

源码中通过自定义的一个RenderObjectWidget和自定义RenderSliver来实现的。
下面我们就来了解一下RenderObjectWidgetRenderSliver

RenderObjectWidget

RenderObjectElement提供配置参数。RenderObjectElement则是包装了提供一个真正为应用提供渲染的RenderObject。

SingleChildRenderObjectWidget

当只有一个child的时,就可以使用这个RenderObjectWidget,它已经为我们实现好了RenderObjectElement,我们只要实现RenderObject的增删改的操作就可以了。
所以实现的核心还是在RenderObject上。

RenderObject

RenderSliver是继承于RenderObject。
RenderObject可以简单的理解成Flutter中的dom模型,主要是负责布局和绘制的。可以继承他实现自己的布局协议。
Flutter中内置实现了两种布局协议。

RenderBox

我们之前使用的非滚动的布局,比如说ColumnRow之类的,都是基于这种布局协议。他提供一个笛卡尔的坐标系的约束。

  • BoxContrains
    它在performLayout方法中,需要根据BoxContrains,计算出对应的Size
  • Size
    描述控件的大小

RenderSliver

  • viewport
    RenderSliver和RenderBox不同。它提供了一个Viewport的概念。
    viewport就相当于一个窗口。窗口内有许多的sliver.他们可以滚动。滚动时,随着他们距离窗口顶部位置(前沿的变化),所以他们的在窗口内的可见部分可能是变化的。
  • SliverConstraints
    它内置的约束是SliverConstraints
    这个约束有个很重要的参数就是SliverConstraints.scrollOffset,用它来编辑滚动的偏移。
  • SliverGeometry
    然后在它在performLayout方法中,需要根据SliverConstraints,计算出对应的SliverGeometry
    SliverGeometry中也有一个很重要的参数是 SliverGeometry.paintExtent
    ,用来描述沿着主轴绘制的范围。
    最终的可见区域就是 在viewport中范围和主轴绘制范围的交集。

自定义Sliver

接着再回头代码

_StatusBarPaddingSliver

//如上面的所诉,我们知道这个`SingleChildRenderObjectWidget`中所做的事情,就是创建返回我们的RenderObject 
class _StatusBarPaddingSliver extends SingleChildRenderObjectWidget {
  const _StatusBarPaddingSliver({
    Key key,
    @required this.maxHeight,
    this.scrollFactor: 5.0,
  }) : assert(maxHeight != null && maxHeight >= 0.0),
       assert(scrollFactor != null && scrollFactor >= 1.0),
       super(key: key);
  
  //我们自己定义的变量。最大高度和滚动的因子
  final double maxHeight;
  final double scrollFactor;
  //创建createRenderObject
  @override
  _RenderStatusBarPaddingSliver createRenderObject(BuildContext context) {
    return new _RenderStatusBarPaddingSliver(
      maxHeight: maxHeight,
      scrollFactor: scrollFactor,
    );
  }

  //更新RenderObject
  @override
  void updateRenderObject(BuildContext context, _RenderStatusBarPaddingSliver renderObject) {
   //这里就是级联的语法,改变状态
    renderObject
      ..maxHeight = maxHeight
      ..scrollFactor = scrollFactor;
  }

//这里是因为了debug模式下,能看到属性,所以写的方法
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
    description.add(new DoubleProperty('maxHeight', maxHeight));
    description.add(new DoubleProperty('scrollFactor', scrollFactor));
  }
}

看到自定义实现的SingleChildRenderObjectWidget,其实很简单,就是实现创建和更新RenderObject的代码就可以了。真正的逻辑在RenderObject中。

_RenderStatusBarPaddingSliver

//继承至`RenderSliver`
class _RenderStatusBarPaddingSliver extends RenderSliver {
  _RenderStatusBarPaddingSliver({
    @required double maxHeight,
    @required double scrollFactor,
  }) : assert(maxHeight != null && maxHeight >= 0.0),
       assert(scrollFactor != null && scrollFactor >= 1.0),
       _maxHeight = maxHeight,
       _scrollFactor = scrollFactor;

  //提供get 和set方法。set方法每次更新时,如果值发生变化了。就需要调用markNeedsLayout,使其重新布局
  // The height of the status bar
  double get maxHeight => _maxHeight;
  double _maxHeight;
  set maxHeight(double value) {
    assert(maxHeight != null && maxHeight >= 0.0);
    if (_maxHeight == value)
      return;
    _maxHeight = value;
    markNeedsLayout();
  }

  // That rate at which this renderer's height shrinks when the scroll
  // offset changes.
  double get scrollFactor => _scrollFactor;
  double _scrollFactor;
  set scrollFactor(double value) {
    assert(scrollFactor != null && scrollFactor >= 1.0);
    if (_scrollFactor == value)
      return;
    _scrollFactor = value;
    markNeedsLayout();
  }

//performLayout 是核心方法。返回一个SliverGeometry来描述这个时候的sliver的大小
  @override
  void performLayout() {
    final double height = (maxHeight - constraints.scrollOffset / scrollFactor).clamp(0.0, maxHeight);
    geometry = new SliverGeometry(
      //paintExtent,便是当前绘制的高度。
      paintExtent: math.min(height, constraints.remainingPaintExtent),
      scrollExtent: maxHeight,
      maxPaintExtent: maxHeight,
    );
  }
}
  • markNeedsLayout
    我们通过set方法改变变量的值时,都需要手动调用这个方法,通知Flutter的渲染框架,在下一帧时,重新布局。
  • performLayout
    RenderSliver的核心方法。返回一个![target-20180816144749.gif](https://upload-images.jianshu.io/upload_images/1877190-fb30b15d0a5403c9.gif?imageMogr2/auto-orient/strip) SliverGeometry来描述这个时候的sliver的大小。
使用

这样,放到CustomScrollView内,就可以感知到约束,进而完成效果了。


整体头部的高度变化

target-20180816144928.gif

可以看到这里的头部滚动是使用SliverPersistentHeader来实现的。而我们之前的头部滚动都是用SliverAppBar来做的。

SliverAppBar

通过跟踪源码,我们发现SliverAppBar其实返回的就是SliverPersistentHeader

SliverAppBar的_SliverAppBarState中的build方法.png

SliverPersistentHeader

会随着sliver滚动到viewport的前缘的距离变化,尺寸随着变化。
它的整体配置,主要还是通过内部的SliverPersistentHeaderDelegate来进行管理。

SliverPersistentHeaderDelegate

这个类中,主要是重写一下方法

build方法
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent);

主要是创建放置在SliverPersistentHeader内的组件。

  1. 这里传递context是sliver的BuildContext
  2. shrinkOffset是从maxExtentminExtent的距离, 表示Sliver当前收缩的偏移量。当shrinkOffset为零时,将在主轴中以maxExtent展现(就是完全展开)。当shrinkOffset等于maxExtentminExtent(正数)之间的差异时,将在主轴中使用minExtent范围呈现内容(最小状态)。该 shrinkOffset会一直在这个范围内的正数。
  3. overlapsContent如果之后有sliver(如果有的话)将在它下面呈现,则该参数为true。如果他下面没有任何内容则为false。通常,这用于决定是否绘制阴影以模拟位于其下方内容之上的内容。通常情况下,如果shrinkOffset处于最大值则为true,否则为false,但这不能保证。有关可以与其无关的值 的示例,请参阅NestedScrollViewoverlapsContent``shrinkOffset
最大最小值
  double get minExtent;
  double get maxExtent;
FloatingHeaderSnapConfiguration
  FloatingHeaderSnapConfiguration get snapConfiguration => null;

SliverPersistentHeader.floating被设置为true,用他可以管理浮动进去的动画效果。这里我们这个头部不是浮动的,所以可以不管。

shouldRebuild方法
  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);

判断两个方法是否不同,如果不同的话,就会重现去创建。

自定义SliverPersistentHeaderDelegate

  • 自定义的原因
    观察发现我们想要的最小高度是大于SliverAppBar的。
    同时,整体的形状变化,我们不需要其他的效果,只要保持和外部滚动的大小一致就可以了。
    我们不使用SliverAppBar。自己简单的来实现一个SliverPersistentHeaderDelegate
代码
//自定义的_SliverAppBarDelegate ,必须输入最小和最大高度
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate({
    @required this.minHeight,
    @required this.maxHeight,
    @required this.child,
  });

  final double minHeight;
  final double maxHeight;
  final Widget child;

  @override double get minExtent => minHeight;
  @override double get maxExtent => math.max(maxHeight, minHeight);
  
  //按照分析,让子组件尽可能占用布局就OK
  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
    return new SizedBox.expand(child: child);
  }

//如果传递的这几个参数变化了,那就重写创建
  @override
  bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
    return maxHeight != oldDelegate.maxHeight
        || minHeight != oldDelegate.minHeight
        || child != oldDelegate.child;
  }

  @override
  String toString() => '_SliverAppBarDelegate';
}
  • build方法
    按照我们上面的分析,只要我们的子控件,竟可能的占用空间就可以了。
    其中SizeBox也是一个RenderObject,而且和上面一样,是SingleChildRenderObjectWidgetSizeBox.expand的方法,就是提供一个尽可能大的组件。
使用
image.png
  • pinnedtrue
    因为我们的头部是最后还是粘性在上面的,所以设置SliverPersistentHeader的pined为true

单页内滑动时的动画效果

target-20180816153526.gif
  • 不同
  1. 这个动画效果和我们之前的动画效果都不同,这意味着我们需要自定义动画。
  2. 而它和我们上面两个自定义的组件也不同,他是一个组件内包括了多个子组件。我们需要在约束变化的过程中,控制多组控件一起变化。

CustomMultiChildLayout

这个Widget可以完全自己掌控布局的排列。我们需要做的是将它的自组件都传递给他,然后实现它的方法,就可以完全的掌握自己的布局了。
完全符合我们的需求。

使用关键点

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

分析动画效果

包括的子组件

我们在这个组件中要安排动画包括 4组SectionCardSectionTitleSectionIndicator

动画的过程
  • 开始状态

    动画的开始状态.png

    开始状态时,SectionCard就是按照column来排列,平均分配屏幕的高度。SectionTitle则是出现在每个SectionCard的中间。SectionIndicator位于右下角。

  • 结束状态

    动画的结束状态.png

    结束状态时,SectionCard就是按照Row来排列,每一列占用了屏幕的宽度。被选中的当前SectionTitle则是出现在被选中的SectionCard的中间。其他的则按照一定间距排列在两边。SectionIndicator位于SectionTitle下面。

自定义MultiChildLayoutDelegate

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);
  }
}
动画的初始

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传入。

LayoutBuilder

上一遍文章,就介绍过,使用LayoutBuilder可以得到变化的约束。来构建动画效果。这里也一样。根据滑动时,变化的约束,来计算百分比。来确定中间状态。

这里的AnimatedWidget会在后面介绍

class _AllSectionsView extends AnimatedWidget {
  _AllSectionsView({
    Key key,
    this.sectionIndex,
    @required this.sections,
    @required this.selectedIndex,
    this.minHeight,
    this.midHeight,
    this.maxHeight,
    this.sectionCards: const <Widget>[],
  }) : assert(sections != null),
       assert(sectionCards != null),
       assert(sectionCards.length == sections.length),
       assert(sectionIndex >= 0 && sectionIndex < sections.length),
       assert(selectedIndex != null),
       assert(selectedIndex.value >= 0.0 && selectedIndex.value < sections.length.toDouble()),
       super(key: key, listenable: selectedIndex);

  final int sectionIndex;
  final List<Section> sections;
  final ValueNotifier<double> selectedIndex;
  final double minHeight;
  final double midHeight;
  final double maxHeight;
  final List<Widget> sectionCards;

  double _selectedIndexDelta(int index) {
    return (index.toDouble() - selectedIndex.value).abs().clamp(0.0, 1.0);
  }

  Widget _build(BuildContext context, BoxConstraints constraints) {
    final Size size = constraints.biggest;

    // 计算中间状态。其实是最大值,到中间值的范围
    final double tColumnToRow =
      1.0 - ((size.height - midHeight) /
             (maxHeight - midHeight)).clamp(0.0, 1.0);
    //中间值到最小值的方法,这个阶段,只会轻微的上移动
    final double tCollapsed =
      1.0 - ((size.height - minHeight) /
             (midHeight - minHeight)).clamp(0.0, 1.0);

  //indicator的透明度需要根据移动尺寸来变化
 double _indicatorOpacity(int index) {
      return 1.0 - _selectedIndexDelta(index) * 0.5;
    }
  //title的透明度需要根据移动尺寸来变化
    double _titleOpacity(int index) {
      return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.5;
    }
    //title的Scale需要根据移动尺寸来变化
    double _titleScale(int index) {
      return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.15;
    }

    final List<Widget> children = new List<Widget>.from(sectionCards);

    for (int index = 0; index < sections.length; index++) {
      final Section section = sections[index];
      //记住,每个child都必须要有位置的LayoutId,方便上面再delegate中识别操作!!
      children.add(new LayoutId(
        id: 'title$index',
        child: new SectionTitle(
          section: section,
          scale: _titleScale(index),
          opacity: _titleOpacity(index),
        ),
      ));
    }

    for (int index = 0; index < sections.length; index++) {
      //记住,每个child都必须要有位置的LayoutId,方便上面再delegate中识别操作!!
      children.add(new LayoutId(
        id: 'indicator$index',
        child: new SectionIndicator(
          opacity: _indicatorOpacity(index),
        ),
      ));
    }

    return new CustomMultiChildLayout(
      delegate: new _AllSectionsLayout(
        translation: new Alignment((selectedIndex.value - sectionIndex) * 2.0 - 1.0, -1.0),
        tColumnToRow: tColumnToRow,
        tCollapsed: tCollapsed,
        cardCount: sections.length,
        selectedIndex: selectedIndex.value,
      ),
      children: children,
    );
  }

  @override
  Widget build(BuildContext context) {
    //通过LayoutBuilder来传递当前正确的约束
    return new LayoutBuilder(builder: _build);
  }
}

横向翻页的效果

头部和下面的部分,都使用Flutter自带提供的PageView就可以实现了。


target-20180816161307.gif

同时上下选中的状态同步

可以看到无论是上面的PageView还是下面的PageView需要做到状态同步。
同时,单页内滑动效果,也需要确定当前选中的那个位置。

滑动事件的监听NotificationListener

Flutter中滑动的组件,都会发送出自己的Notification。之前的文章介绍过,只要在要监听的组件外面套一层NotificationListener就可以监听到对应的事件。

ScrollerController

可以滚动的部件,基本都有一个ScrollController来控制和查询滑动的状态。
监听的滑动事件过程中,我们可以通过它来完成两个类的状态同步。

ValueNotifier & AnimatedWidget

  • ValueNotifier
    因为我们还需要在单页内滑动的效果同步到我们选中的位置。所以,我们可以使用ValueNotifier。之前也介绍过,可以设置这个值得监听,每次改变,都会通知观察者。
  • AnimatedWidget
    AnimatedWidget其实是一个帮助类。我们可以给他我们可以监听的属性。(动画或者ValueNotifier/ChangeNotifier),每当监听的属性发送通知时,都会自动调用setState的方法进行rebuild
    使用它,就避免了自己手动写注册监听的事件。

同时,当他改变后,我们需要监听的Widget,重写setState进行rebuild。
我们使用,就可以避免自己手动实现生命周期的监听和取消监听这样的模板化的代码了。

代码

  • 初始化
    初始化上面需要监听的变量和controller
  //_AnimationDemoHomeState文件中
 final PageController _headingPageController = new PageController();
  final PageController _detailsPageController = new PageController();
  ValueNotifier<double> selectedIndex = new ValueNotifier<double>(0.0);
  • 监听事件
    在每个PageView的外层套用NotificationListener来监听事件。之前介绍过。这是常规操作。


    PageView的外层来监听当前pageView的滚动事件.png
  • 处理Notification监听事件
    就是监听事件,然后触发ValueNotifier的监听事件,和使用controller同步上下滚动的状态。

bool _handlePageNotification(ScrollNotification notification, PageController leader, PageController follower) {
    if (notification.depth == 0 && notification is ScrollUpdateNotification) {
      //修改selectedIndex 会触发监听
      selectedIndex.value = leader.page;
      if (follower.page != leader.page)
        //如果两个Page不想都能,就让follower的一方,滚动过去
        follower.position.jumpToWithoutSettling(leader.position.pixels); // ignore: deprecated_member_use
    }
    return false;
  }
  • 处理ValueNotifier的监听
    上面分析过AnimatedWidget的功能。因为我们的头部几个组件,也需要这里同步
    状态。所以让_AllSectionsView继承它。这样,就避免写重复的注册监听这个时间的模板化代码(在生命周期里,initState.didChangeDependes注册这个监听,在dispose内,取消这个监听。)
    这样上面一改变这个ValueNotifier的值,就会直接出发_AllSectionsViewrebuild。来完成动画效果。

滚动时的物理效果

ScrollPhysics

这些滚动组件的物理滚动效果都是通过ScrollPhysics来进行配置的。

Flutter自带的

自动的ScrollPhysics就有4个。

  1. BouncingScrollPhysics,弹性的滚动效果。
  2. ClampingScrollPhysics,正常的滚动效果,没有弹性。
  3. NeverScrollableScrollPhysics,不滚动。
  4. AlwaysScrollableScrollPhysics,在Android上和ClampingScrollPhysics一样,在IOS上和BouncingScrollPhysics一样。

动画分析

这个动画中,有两种处理。

PageView

因为上下都是PageView,当单页内的动画在初始状态和结束状态(中间)中间。是不能切换PageView的。当高度小于时,才能切换。

  • 监听滑动的距离


    监听整个的滑动情况.png

    因为要监听CustomScrollView的滑动情况,所以要套在它的外层。

  • 进行切换

  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;
  }

自定义ScrollPhysics

CustomScrollView滑动时,当方向是朝着上,而且放手时,会自动吸附到中间位置。
吸附的动画效果,本身没有提供。所以我们需要自己重写。

Simulation

可以理解成动画进行的函数。

Flutter中自带了有下面几种。

BouncingScrollSimulationBounce弹性的滚动模拟
ClampedSimulation
ClampingScrollSimulation*
FrictionSimulation摩擦参数的的滚动模拟
GravitySimulation类似重力的模,
SpringSimulation弹簧弹力的模拟。

我们这里,通过自定义ScrollPhysics 返回对应的SpringSimulation就满足我们的效果了。

代码
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) {
    //去到滑动的速度和默认最小Fling速度的最大值
    final double velocity = math.max(dragVelocity, minFlingVelocity);
    //创建ScrollSpringSimulation。
    return new ScrollSpringSimulation(spring, offset, midScrollOffset, velocity, tolerance: tolerance);
  }

  //粘性到原点的移动
  Simulation _toZeroScrollOffsetSimulation(double offset, double dragVelocity) {
    //去到滑动的速度和默认最小Fling速度的最大值
    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) {
      //通过这方法,可以快速拿到终止的位置
      final double simulationEnd = simulation.x(double.infinity);
      //当终止的位置大于midScrollOffset时,可以不进行处理,正常滑动
      if (simulationEnd >= midScrollOffset)
        return simulation;
      //当小于mid,而且速度方向向上的话,就粘性到中间
      if (dragVelocity > 0.0)
        return _toMidScrollOffsetSimulation(offset, dragVelocity);
      //当小于mid,而且速度方向向下的话,就粘性到底部
      if (dragVelocity < 0.0)
        return _toZeroScrollOffsetSimulation(offset, dragVelocity);
    } else {
      //如果停止时,没有触发任何滑动效果,那么,当滑动在上部时,而且接近mid,就会粘性到mid
      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;
  }
}

总结

通过解析,我们除了明白复杂的动画效果,我们如何进行自定义外,我们可以有两个基础的概念

Scrollable

Scrollable的部件,滚动效果由physic配置,滚动控制由controller配置。

Widget & RenderElement & RenderObject

这边文章通过自定义的SingleChildRenderObjectWidget,返回自定义的RenderObject。来完全控制我们的组件的布局也可以看出。

  • RenderObjectWidget
    RenderObjectWidget内主要负责对RenderObject的配置。配置了他的更新规则和创建规则。

  • RenderObject
    而RenderObject则进行真实的布局和绘制。真实的 布局代码是在它内完成的。
    而flutter内置的协议RenderSliver则是在performLayout方法中,通过SliverContraints这种约束,来确定返回SliverGeometry就可以了。

  • RenderObjectElement
    这里没有看到的是这个类,他主要进行dom的diff算法。因为我们继承的SingleChildRenderObjectWidget已经为我们创建好了对应的SingleChildRenderObjectElement了。
    它内负责的就是真实的增删改的代码。

三者的关系理解

结合Vue和React
G2.png
回顾使用以来的控件

同时,我们也可以进一步了解下面张图的意思


Widget&Element&RenderObject.png
  • 组合型的控件
    就是我们最常用的控件。
  • 代理型的控件
    这类控件,我们在入门的第二遍文章,就介绍过。用它来保存状态的。
  • 展示型
    展示型,我们这边文章里面遇到了。其实就是可以自己去继承定义这样的控件,完全控制的布局规则和绘制规则。

最后

介绍到这边文章,我们已经大体对Flutter的界面开发有了一个相对全面的了解。
后续,我们会继续从几个深入去探究。

  1. 一个是它的源码实现,看看他到底是怎么实现的。
  2. 另个就是会去搭建真实的项目,看看如何写一个Reactive 的Flutter项目。
  3. 混合工程的编译流程

参考

Flutter SDK doc
Flutter中的布局绘制流程简析(一)
深入了解Flutter界面开发(强烈推荐)

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1psdgylx8dufa

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,967评论 3 119
  • 以下代码基本参考于 flutter_gallery中的animation_demo示例。(可以结合本文看源码) 题...
    deep_sadness阅读 927评论 0 1
  • 我遇见他之前,从未想到过要结婚;我娶了她几十年,从未后悔娶她,也未想过要娶别的女人,--钱钟书 他,并不是我遇见最...
    Confident张阅读 324评论 0 0
  • 又到周五,又是一个新的月份开始,每年的最后一个月,今天,空气质量格外的好,因为从前天开始降温,胶东已经下雪,昨晚认...
    星之梦lyx阅读 203评论 0 0
  • 昨天看新一季的奇葩大会被一个月入10万的00后征服了,不仅因为她的简单执着,还在于她可爱的小自信。 ...
    lssf阅读 376评论 2 1