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

weixin_34007291發表於2018-08-16

以下程式碼基本參考於 flutter_gallery中的animation_demo示例。(可以結合本文看原始碼)
整體動畫效果預覽

1877190-fcb0ff178c844cdd.gif
animation.gif

頂部的statusBar部分的高度變化

1877190-35d1b04e5c2aa0ee.gif
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內,就可以感知到約束,進而完成效果了。


整體頭部的高度變化

1877190-6d752b89ffd714f0.gif
target-20180816144928.gif

可以看到這裡的頭部滾動是使用SliverPersistentHeader來實現的。而我們之前的頭部滾動都是用SliverAppBar來做的。

SliverAppBar

通過跟蹤原始碼,我們發現SliverAppBar其實返回的就是SliverPersistentHeader

1877190-803203cc0a408bc3.png
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的方法,就是提供一個儘可能大的元件。
使用
1877190-2917f147b16aad35.png
image.png
  • pinnedtrue
    因為我們的頭部是最後還是粘性在上面的,所以設定SliverPersistentHeader的pined為true

單頁內滑動時的動畫效果

1877190-b9c296c2eaf8ede6.gif
target-20180816153526.gif
  • 不同
  1. 這個動畫效果和我們之前的動畫效果都不同,這意味著我們需要自定義動畫。
  2. 而它和我們上面兩個自定義的元件也不同,他是一個元件內包括了多個子元件。我們需要在約束變化的過程中,控制多組控制元件一起變化。

CustomMultiChildLayout

這個Widget可以完全自己掌控佈局的排列。我們需要做的是將它的自元件都傳遞給他,然後實現它的方法,就可以完全的掌握自己的佈局了。
完全符合我們的需求。

使用關鍵點

  1. 自定義MultiChildLayoutDelegate來自己實現佈局
  2. 他的每個child都需要用layoutId來包裹,並且分配給他們的id,都必須是唯一的。

分析動畫效果

包括的子元件

我們在這個元件中要安排動畫包括 4組SectionCardSectionTitleSectionIndicator

動畫的過程
  • 開始狀態

    1877190-3c0f1635030e7119.png
    動畫的開始狀態.png

    開始狀態時,SectionCard就是按照column來排列,平均分配螢幕的高度。SectionTitle則是出現在每個SectionCard的中間。SectionIndicator位於右下角。

  • 結束狀態

    1877190-254b2333bb8d7ce3.png
    動畫的結束狀態.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就可以實現了。


1877190-f4e27536d1d83c86.gif
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來監聽事件。之前介紹過。這是常規操作。


    1877190-2d7b208c0b87e735.png
    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的。當高度小於時,才能切換。

  • 監聽滑動的距離


    1877190-47c05ef3f45befcf.png
    監聽整個的滑動情況.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
1877190-9fc862b4cff46ba5.png
G2.png
回顧使用以來的控制元件

同時,我們也可以進一步瞭解下面張圖的意思


1877190-8fde1b18ea4e75d8.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

相關文章