以下代碼基本參考于 flutter_gallery中的animation_demo示例。(可以結(jié)合本文看源碼)
整體動畫效果預(yù)覽

頂部的statusBar部分的高度變化

源碼中通過自定義的一個RenderObjectWidget和自定義RenderSliver來實現(xiàn)的。
下面我們就來了解一下RenderObjectWidget和RenderSliver。
RenderObjectWidget
為RenderObjectElement提供配置參數(shù)。RenderObjectElement則是包裝了提供一個真正為應(yīng)用提供渲染的RenderObject。
SingleChildRenderObjectWidget
當(dāng)只有一個child的時,就可以使用這個RenderObjectWidget,它已經(jīng)為我們實現(xiàn)好了RenderObjectElement,我們只要實現(xiàn)RenderObject的增刪改的操作就可以了。
所以實現(xiàn)的核心還是在RenderObject上。
RenderObject
RenderSliver是繼承于RenderObject。
RenderObject可以簡單的理解成Flutter中的dom模型,主要是負(fù)責(zé)布局和繪制的??梢岳^承他實現(xiàn)自己的布局協(xié)議。
Flutter中內(nèi)置實現(xiàn)了兩種布局協(xié)議。
RenderBox
我們之前使用的非滾動的布局,比如說Column、Row之類的,都是基于這種布局協(xié)議。他提供一個笛卡爾的坐標(biāo)系的約束。
- BoxContrains
它在performLayout方法中,需要根據(jù)BoxContrains,計算出對應(yīng)的Size。 - Size
描述控件的大小
RenderSliver
- viewport
RenderSliver和RenderBox不同。它提供了一個Viewport的概念。
viewport就相當(dāng)于一個窗口。窗口內(nèi)有許多的sliver.他們可以滾動。滾動時,隨著他們距離窗口頂部位置(前沿的變化),所以他們的在窗口內(nèi)的可見部分可能是變化的。 - SliverConstraints
它內(nèi)置的約束是SliverConstraints。
這個約束有個很重要的參數(shù)就是SliverConstraints.scrollOffset,用它來編輯滾動的偏移。 - SliverGeometry
然后在它在performLayout方法中,需要根據(jù)SliverConstraints,計算出對應(yīng)的SliverGeometry
。SliverGeometry中也有一個很重要的參數(shù)是 SliverGeometry.paintExtent
,用來描述沿著主軸繪制的范圍。
最終的可見區(qū)域就是 在viewport中范圍和主軸繪制范圍的交集。
自定義Sliver
接著再回頭代碼
_StatusBarPaddingSliver
//如上面的所訴,我們知道這個`SingleChildRenderObjectWidget`中所做的事情,就是創(chuàng)建返回我們的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;
//創(chuàng)建createRenderObject
@override
_RenderStatusBarPaddingSliver createRenderObject(BuildContext context) {
return new _RenderStatusBarPaddingSliver(
maxHeight: maxHeight,
scrollFactor: scrollFactor,
);
}
//更新RenderObject
@override
void updateRenderObject(BuildContext context, _RenderStatusBarPaddingSliver renderObject) {
//這里就是級聯(lián)的語法,改變狀態(tài)
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));
}
}
看到自定義實現(xiàn)的SingleChildRenderObjectWidget,其實很簡單,就是實現(xiàn)創(chuàng)建和更新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方法每次更新時,如果值發(fā)生變化了。就需要調(diào)用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,便是當(dāng)前繪制的高度。
paintExtent: math.min(height, constraints.remainingPaintExtent),
scrollExtent: maxHeight,
maxPaintExtent: maxHeight,
);
}
}
- markNeedsLayout
我們通過set方法改變變量的值時,都需要手動調(diào)用這個方法,通知Flutter的渲染框架,在下一幀時,重新布局。 - performLayout
RenderSliver的核心方法。返回一個 SliverGeometry來描述這個時候的sliver的大小。
使用
這樣,放到CustomScrollView內(nèi),就可以感知到約束,進(jìn)而完成效果了。
整體頭部的高度變化

可以看到這里的頭部滾動是使用SliverPersistentHeader來實現(xiàn)的。而我們之前的頭部滾動都是用SliverAppBar來做的。
SliverAppBar
通過跟蹤源碼,我們發(fā)現(xiàn)SliverAppBar其實返回的就是SliverPersistentHeader。

SliverPersistentHeader
會隨著sliver滾動到viewport的前緣的距離變化,尺寸隨著變化。
它的整體配置,主要還是通過內(nèi)部的SliverPersistentHeaderDelegate來進(jìn)行管理。
SliverPersistentHeaderDelegate
這個類中,主要是重寫一下方法
build方法
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent);
主要是創(chuàng)建放置在SliverPersistentHeader內(nèi)的組件。
- 這里傳遞
context是sliver的BuildContext - 這
shrinkOffset是從maxExtent到minExtent的距離, 表示Sliver當(dāng)前收縮的偏移量。當(dāng)shrinkOffset為零時,將在主軸中以maxExtent展現(xiàn)(就是完全展開)。當(dāng)shrinkOffset等于maxExtent和minExtent(正數(shù))之間的差異時,將在主軸中使用minExtent范圍呈現(xiàn)內(nèi)容(最小狀態(tài))。該shrinkOffset會一直在這個范圍內(nèi)的正數(shù)。 -
overlapsContent如果之后有sliver(如果有的話)將在它下面呈現(xiàn),則該參數(shù)為true。如果他下面沒有任何內(nèi)容則為false。通常,這用于決定是否繪制陰影以模擬位于其下方內(nèi)容之上的內(nèi)容。通常情況下,如果shrinkOffset處于最大值則為true,否則為false,但這不能保證。有關(guān)可以與其無關(guān)的值 的示例,請參閱NestedScrollView。overlapsContent``shrinkOffset
最大最小值
double get minExtent;
double get maxExtent;
FloatingHeaderSnapConfiguration
FloatingHeaderSnapConfiguration get snapConfiguration => null;
當(dāng)SliverPersistentHeader.floating被設(shè)置為true,用他可以管理浮動進(jìn)去的動畫效果。這里我們這個頭部不是浮動的,所以可以不管。
shouldRebuild方法
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);
判斷兩個方法是否不同,如果不同的話,就會重現(xiàn)去創(chuàng)建。
自定義SliverPersistentHeaderDelegate
- 自定義的原因
觀察發(fā)現(xiàn)我們想要的最小高度是大于SliverAppBar的。
同時,整體的形狀變化,我們不需要其他的效果,只要保持和外部滾動的大小一致就可以了。
我們不使用SliverAppBar。自己簡單的來實現(xiàn)一個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);
}
//如果傳遞的這幾個參數(shù)變化了,那就重寫創(chuàng)建
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return maxHeight != oldDelegate.maxHeight
|| minHeight != oldDelegate.minHeight
|| child != oldDelegate.child;
}
@override
String toString() => '_SliverAppBarDelegate';
}
- build方法
按照我們上面的分析,只要我們的子控件,竟可能的占用空間就可以了。
其中SizeBox也是一個RenderObject,而且和上面一樣,是SingleChildRenderObjectWidget。SizeBox.expand的方法,就是提供一個盡可能大的組件。
使用

-
pinned為true
因為我們的頭部是最后還是粘性在上面的,所以設(shè)置SliverPersistentHeader的pined為true
單頁內(nèi)滑動時的動畫效果

- 不同
- 這個動畫效果和我們之前的動畫效果都不同,這意味著我們需要自定義動畫。
- 而它和我們上面兩個自定義的組件也不同,他是一個組件內(nèi)包括了多個子組件。我們需要在約束變化的過程中,控制多組控件一起變化。
CustomMultiChildLayout
這個Widget可以完全自己掌控布局的排列。我們需要做的是將它的自組件都傳遞給他,然后實現(xiàn)它的方法,就可以完全的掌握自己的布局了。
完全符合我們的需求。
使用關(guān)鍵點
- 自定義MultiChildLayoutDelegate來自己實現(xiàn)布局
- 他的每個child都需要用layoutId來包裹,并且分配給他們的id,都必須是唯一的。
分析動畫效果
包括的子組件
我們在這個組件中要安排動畫包括 4組SectionCard、SectionTitle、SectionIndicator。
動畫的過程
-
開始狀態(tài)
動畫的開始狀態(tài).png
開始狀態(tài)時,SectionCard就是按照column來排列,平均分配屏幕的高度。SectionTitle則是出現(xiàn)在每個SectionCard的中間。SectionIndicator位于右下角。 -
結(jié)束狀態(tài)
動畫的結(jié)束狀態(tài).png
結(jié)束狀態(tài)時,SectionCard就是按照Row來排列,每一列占用了屏幕的寬度。被選中的當(dāng)前SectionTitle則是出現(xiàn)在被選中的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);
// 定義好初始的位置和結(jié)束的位置,就可以使用這個lerp函數(shù),輕松的找到中間狀態(tài)值
//rect 的 shift ,相當(dāng)于 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的初始狀態(tài)column為前綴的變量。
- 高度
就是按照我們看到的,豎排的情況下,每個Card的高度是整個appBar高度的4分之一。 - left
統(tǒng)一的位置。 - 寬度
去掉left部分的,寬度 - Offset
Offset需要確定的位置,需要和選定的坐標(biāo)協(xié)同。選定的Index,畢竟出現(xiàn)在當(dāng)前位置。就是他的Offset的x,必須和自己的left相反,這樣才能在第一個位置。
它是用Aligment.alongSize來進(jìn)行轉(zhuǎn)換。Alignment(-1.0, -1.0)就代表左上角。Alignment(1.0, 1.0)代表矩形的右下角。整個Aligment相當(dāng)于一個邊長為2,中心點在原點的正方形。
需要讓index== selectedIndex的card的Aligment為左上角Alignment(1.0, 1.0)的狀態(tài)。然后其他對應(yīng)的進(jìn)行偏移。
動畫的結(jié)尾
card的最終狀態(tài)row為前綴的變量
高度
就是整個的高度left
就是選中card的偏移量。寬度
就是整個的寬度offset
同上。
確定中間狀態(tài)
-
tColumnToRow
整體的動畫,在Flutter中有很方便的lerp函數(shù)可以確定中間的狀態(tài)。只要傳入我們進(jìn)度的百分比就可以。這個百分比可以由滑動的過程中的offset傳入。
LayoutBuilder
上一遍文章,就介紹過,使用LayoutBuilder可以得到變化的約束。來構(gòu)建動畫效果。這里也一樣。根據(jù)滑動時,變化的約束,來計算百分比。來確定中間狀態(tài)。
這里的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;
// 計算中間狀態(tài)。其實是最大值,到中間值的范圍
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的透明度需要根據(jù)移動尺寸來變化
double _indicatorOpacity(int index) {
return 1.0 - _selectedIndexDelta(index) * 0.5;
}
//title的透明度需要根據(jù)移動尺寸來變化
double _titleOpacity(int index) {
return 1.0 - _selectedIndexDelta(index) * tColumnToRow * 0.5;
}
//title的Scale需要根據(jù)移動尺寸來變化
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來傳遞當(dāng)前正確的約束
return new LayoutBuilder(builder: _build);
}
}
橫向翻頁的效果
頭部和下面的部分,都使用Flutter自帶提供的PageView就可以實現(xiàn)了。

同時上下選中的狀態(tài)同步
可以看到無論是上面的PageView還是下面的PageView需要做到狀態(tài)同步。
同時,單頁內(nèi)滑動效果,也需要確定當(dāng)前選中的那個位置。
滑動事件的監(jiān)聽NotificationListener
Flutter中滑動的組件,都會發(fā)送出自己的Notification。之前的文章介紹過,只要在要監(jiān)聽的組件外面套一層NotificationListener就可以監(jiān)聽到對應(yīng)的事件。
ScrollerController
可以滾動的部件,基本都有一個ScrollController來控制和查詢滑動的狀態(tài)。
監(jiān)聽的滑動事件過程中,我們可以通過它來完成兩個類的狀態(tài)同步。
ValueNotifier & AnimatedWidget
- ValueNotifier
因為我們還需要在單頁內(nèi)滑動的效果同步到我們選中的位置。所以,我們可以使用ValueNotifier。之前也介紹過,可以設(shè)置這個值得監(jiān)聽,每次改變,都會通知觀察者。 - AnimatedWidget
AnimatedWidget其實是一個幫助類。我們可以給他我們可以監(jiān)聽的屬性。(動畫或者ValueNotifier/ChangeNotifier),每當(dāng)監(jiān)聽的屬性發(fā)送通知時,都會自動調(diào)用setState的方法進(jìn)行rebuild。
使用它,就避免了自己手動寫注冊監(jiān)聽的事件。
同時,當(dāng)他改變后,我們需要監(jiān)聽的Widget,重寫setState進(jìn)行rebuild。
我們使用,就可以避免自己手動實現(xiàn)生命周期的監(jiān)聽和取消監(jiān)聽這樣的模板化的代碼了。
代碼
- 初始化
初始化上面需要監(jiān)聽的變量和controller
//_AnimationDemoHomeState文件中
final PageController _headingPageController = new PageController();
final PageController _detailsPageController = new PageController();
ValueNotifier<double> selectedIndex = new ValueNotifier<double>(0.0);
-
監(jiān)聽事件
在每個PageView的外層套用NotificationListener來監(jiān)聽事件。之前介紹過。這是常規(guī)操作。
PageView的外層來監(jiān)聽當(dāng)前pageView的滾動事件.png 處理Notification監(jiān)聽事件
就是監(jiān)聽事件,然后觸發(fā)ValueNotifier的監(jiān)聽事件,和使用controller同步上下滾動的狀態(tài)。
bool _handlePageNotification(ScrollNotification notification, PageController leader, PageController follower) {
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
//修改selectedIndex 會觸發(fā)監(jiān)聽
selectedIndex.value = leader.page;
if (follower.page != leader.page)
//如果兩個Page不想都能,就讓follower的一方,滾動過去
follower.position.jumpToWithoutSettling(leader.position.pixels); // ignore: deprecated_member_use
}
return false;
}
- 處理ValueNotifier的監(jiān)聽
上面分析過AnimatedWidget的功能。因為我們的頭部幾個組件,也需要這里同步
狀態(tài)。所以讓_AllSectionsView繼承它。這樣,就避免寫重復(fù)的注冊監(jiān)聽這個時間的模板化代碼(在生命周期里,initState.didChangeDependes注冊這個監(jiān)聽,在dispose內(nèi),取消這個監(jiān)聽。)
這樣上面一改變這個ValueNotifier的值,就會直接出發(fā)_AllSectionsViewrebuild。來完成動畫效果。
滾動時的物理效果
ScrollPhysics
這些滾動組件的物理滾動效果都是通過ScrollPhysics來進(jìn)行配置的。
Flutter自帶的
自動的ScrollPhysics就有4個。
-
BouncingScrollPhysics,彈性的滾動效果。 -
ClampingScrollPhysics,正常的滾動效果,沒有彈性。 - NeverScrollableScrollPhysics,不滾動。
-
AlwaysScrollableScrollPhysics,在Android上和ClampingScrollPhysics一樣,在IOS上和BouncingScrollPhysics一樣。
動畫分析
這個動畫中,有兩種處理。
PageView
因為上下都是PageView,當(dāng)單頁內(nèi)的動畫在初始狀態(tài)和結(jié)束狀態(tài)(中間)中間。是不能切換PageView的。當(dāng)高度小于時,才能切換。
-
監(jiān)聽滑動的距離
監(jiān)聽整個的滑動情況.png
因為要監(jiān)聽CustomScrollView的滑動情況,所以要套在它的外層。
進(jìn)行切換
bool _handleScrollNotification(ScrollNotification notification, double midScrollOffset) {
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
//這里就是切換的代碼了。超過中間的高度,則開始滾動,復(fù)制不能滾動。
final ScrollPhysics physics = _scrollController.position.pixels >= midScrollOffset
? const PageScrollPhysics()
: const NeverScrollableScrollPhysics();
if (physics != _headingScrollPhysics) {
setState(() {
_headingScrollPhysics = physics;
});
}
}
return false;
}
自定義ScrollPhysics
CustomScrollView滑動時,當(dāng)方向是朝著上,而且放手時,會自動吸附到中間位置。
吸附的動畫效果,本身沒有提供。所以我們需要自己重寫。
Simulation
可以理解成動畫進(jìn)行的函數(shù)。
Flutter中自帶了有下面幾種。
BouncingScrollSimulationBounce彈性的滾動模擬
ClampedSimulation
ClampingScrollSimulation*
FrictionSimulation摩擦參數(shù)的的滾動模擬
GravitySimulation類似重力的模,
SpringSimulation彈簧彈力的模擬。
我們這里,通過自定義ScrollPhysics 返回對應(yīng)的SpringSimulation就滿足我們的效果了。
代碼
class _SnappingScrollPhysics extends ClampingScrollPhysics {
const _SnappingScrollPhysics({
ScrollPhysics parent,
@required this.midScrollOffset,
}) : assert(midScrollOffset != null),
super(parent: parent);
//中間的偏移量。用于區(qū)分
final double midScrollOffset;
@override
_SnappingScrollPhysics applyTo(ScrollPhysics ancestor) {
return new _SnappingScrollPhysics(parent: buildParent(ancestor), midScrollOffset: midScrollOffset);
}
//粘性到中間的移動
Simulation _toMidScrollOffsetSimulation(double offset, double dragVelocity) {
//去到滑動的速度和默認(rèn)最小Fling速度的最大值
final double velocity = math.max(dragVelocity, minFlingVelocity);
//創(chuàng)建ScrollSpringSimulation。
return new ScrollSpringSimulation(spring, offset, midScrollOffset, velocity, tolerance: tolerance);
}
//粘性到原點的移動
Simulation _toZeroScrollOffsetSimulation(double offset, double dragVelocity) {
//去到滑動的速度和默認(rèn)最小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);
//得到當(dāng)前的偏移
final double offset = position.pixels;
if (simulation != null) {
//通過這方法,可以快速拿到終止的位置
final double simulationEnd = simulation.x(double.infinity);
//當(dāng)終止的位置大于midScrollOffset時,可以不進(jìn)行處理,正?;瑒? if (simulationEnd >= midScrollOffset)
return simulation;
//當(dāng)小于mid,而且速度方向向上的話,就粘性到中間
if (dragVelocity > 0.0)
return _toMidScrollOffsetSimulation(offset, dragVelocity);
//當(dāng)小于mid,而且速度方向向下的話,就粘性到底部
if (dragVelocity < 0.0)
return _toZeroScrollOffsetSimulation(offset, dragVelocity);
} else {
//如果停止時,沒有觸發(fā)任何滑動效果,那么,當(dāng)滑動在上部時,而且接近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;
}
}
總結(jié)
通過解析,我們除了明白復(fù)雜的動畫效果,我們?nèi)绾芜M(jìn)行自定義外,我們可以有兩個基礎(chǔ)的概念
Scrollable
Scrollable的部件,滾動效果由physic配置,滾動控制由controller配置。
Widget & RenderElement & RenderObject
這邊文章通過自定義的SingleChildRenderObjectWidget,返回自定義的RenderObject。來完全控制我們的組件的布局也可以看出。
RenderObjectWidget
RenderObjectWidget內(nèi)主要負(fù)責(zé)對RenderObject的配置。配置了他的更新規(guī)則和創(chuàng)建規(guī)則。RenderObject
而RenderObject則進(jìn)行真實的布局和繪制。真實的 布局代碼是在它內(nèi)完成的。
而flutter內(nèi)置的協(xié)議RenderSliver則是在performLayout方法中,通過SliverContraints這種約束,來確定返回SliverGeometry就可以了。RenderObjectElement
這里沒有看到的是這個類,他主要進(jìn)行dom的diff算法。因為我們繼承的SingleChildRenderObjectWidget已經(jīng)為我們創(chuàng)建好了對應(yīng)的SingleChildRenderObjectElement了。
它內(nèi)負(fù)責(zé)的就是真實的增刪改的代碼。
三者的關(guān)系理解
結(jié)合Vue和React

回顧使用以來的控件
同時,我們也可以進(jìn)一步了解下面張圖的意思

- 組合型的控件
就是我們最常用的控件。 - 代理型的控件
這類控件,我們在入門的第二遍文章,就介紹過。用它來保存狀態(tài)的。 - 展示型
展示型,我們這邊文章里面遇到了。其實就是可以自己去繼承定義這樣的控件,完全控制的布局規(guī)則和繪制規(guī)則。
最后
介紹到這邊文章,我們已經(jīng)大體對Flutter的界面開發(fā)有了一個相對全面的了解。
后續(xù),我們會繼續(xù)從幾個深入去探究。
- 一個是它的源碼實現(xiàn),看看他到底是怎么實現(xiàn)的。
- 另個就是會去搭建真實的項目,看看如何寫一個Reactive 的Flutter項目。
- 混合工程的編譯流程
參考
Flutter SDK doc
Flutter中的布局繪制流程簡析(一)
深入了解Flutter界面開發(fā)(強(qiáng)烈推薦)
我的博客即將搬運同步至騰訊云+社區(qū),邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1psdgylx8dufa



