Flutter開發實戰分析-animation_demo解析導讀

如下代碼基本參考於 flutter_gallery中的animation_demo示例。(能夠結合本文看源碼)html

總體動畫效果預覽 git

animation.gif

頂部的statusBar部分的高度變化

target-20180816144749.gif

源碼中經過自定義的一個RenderObjectWidget和自定義RenderSliver來實現的。 下面咱們就來了解一下RenderObjectWidgetRenderSlivergithub

RenderObjectWidget

RenderObjectElement提供配置參數。RenderObjectElement則是包裝了提供一個真正爲應用提供渲染的RenderObject。算法

SingleChildRenderObjectWidget

當只有一個child的時,就可使用這個RenderObjectWidget,它已經爲咱們實現好了RenderObjectElement,咱們只要實現RenderObject的增刪改的操做就能夠了。 因此實現的核心仍是在RenderObject上。spring

RenderObject

RenderSliver是繼承於RenderObject。 RenderObject能夠簡單的理解成Flutter中的dom模型,主要是負責佈局和繪製的。能夠繼承他實現本身的佈局協議。 Flutter中內置實現了兩種佈局協議。segmentfault

RenderBox

咱們以前使用的非滾動的佈局,好比說ColumnRow之類的,都是基於這種佈局協議。他提供一個笛卡爾的座標系的約束。markdown

  • BoxContrains 它在performLayout方法中,須要根據BoxContrains,計算出對應的Size
  • Size 描述控件的大小

RenderSliver

  • viewport RenderSliver和RenderBox不一樣。它提供了一個Viewport的概念。 viewport就至關於一個窗口。窗口內有許多的sliver.他們能夠滾動。滾動時,隨着他們距離窗口頂部位置(前沿的變化),因此他們的在窗口內的可見部分多是變化的。
  • SliverConstraints 它內置的約束是SliverConstraints。 這個約束有個很重要的參數就是SliverConstraints.scrollOffset,用它來編輯滾動的偏移。
  • SliverGeometry 而後在它在performLayout方法中,須要根據SliverConstraints,計算出對應的SliverGeometrySliverGeometry中也有一個很重要的參數是 SliverGeometry.paintExtent ,用來描述沿着主軸繪製的範圍。 最終的可見區域就是 在viewport中範圍和主軸繪製範圍的交集。

自定義Sliver

接着再回頭代碼app

_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內,就能夠感知到約束,進而完成效果了。dom


總體頭部的高度變化

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中自帶了有下面幾種。

咱們這裏,經過自定義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的界面開發有了一個相對全面的瞭解。

參考

Flutter SDK doc

Flutter中的佈局繪製流程簡析(一)

深刻了解Flutter界面開發(強烈推薦)

相關文章
相關標籤/搜索