Flutter開發實戰分析-animation_demo解析導讀
以下程式碼基本參考於 flutter_gallery中的animation_demo示例。(可以結合本文看原始碼)
整體動畫效果預覽
頂部的statusBar部分的高度變化
原始碼中通過自定義的一個RenderObjectWidget
和自定義RenderSliver
來實現的。
下面我們就來了解一下RenderObjectWidget
和RenderSliver
。
RenderObjectWidget
為RenderObjectElement提供配置引數。RenderObjectElement
則是包裝了提供一個真正為應用提供渲染的RenderObject。
SingleChildRenderObjectWidget
當只有一個child的時,就可以使用這個RenderObjectWidget
,它已經為我們實現好了RenderObjectElement
,我們只要實現RenderObject
的增刪改的操作就可以了。
所以實現的核心還是在RenderObject
上。
RenderObject
RenderSliver是繼承於RenderObject。
RenderObject可以簡單的理解成Flutter中的dom模型,主要是負責佈局和繪製的。可以繼承他實現自己的佈局協議。
Flutter中內建實現了兩種佈局協議。
RenderBox
我們之前使用的非滾動的佈局,比如說Column
、Row
之類的,都是基於這種佈局協議。他提供一個笛卡爾的座標系的約束。
- 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內,就可以感知到約束,進而完成效果了。
整體頭部的高度變化
可以看到這裡的頭部滾動是使用SliverPersistentHeader
來實現的。而我們之前的頭部滾動都是用SliverAppBar
來做的。
SliverAppBar
通過跟蹤原始碼,我們發現SliverAppBar
其實返回的就是SliverPersistentHeader
。
SliverPersistentHeader
會隨著sliver滾動到viewport的前緣的距離變化,尺寸隨著變化。
它的整體配置,主要還是通過內部的SliverPersistentHeaderDelegate
來進行管理。
SliverPersistentHeaderDelegate
這個類中,主要是重寫一下方法
build方法
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent);
主要是建立放置在SliverPersistentHeader內的元件。
- 這裡傳遞
context
是sliver的BuildContext
- 這
shrinkOffset
是從maxExtent
到minExtent
的距離, 表示Sliver當前收縮的偏移量。當shrinkOffset
為零時,將在主軸中以maxExtent
展現(就是完全展開)。當shrinkOffset
等於maxExtent
和minExtent
(正數)之間的差異時,將在主軸中使用minExtent
範圍呈現內容(最小狀態)。該shrinkOffset
會一直在這個範圍內的正數。 -
overlapsContent
如果之後有sliver(如果有的話)將在它下面呈現,則該引數為true。如果他下面沒有任何內容則為false。通常,這用於決定是否繪製陰影以模擬位於其下方內容之上的內容。通常情況下,如果shrinkOffset
處於最大值則為true,否則為false,但這不能保證。有關可以與其無關的值 的示例,請參閱NestedScrollView。overlapsContent``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
,而且和上面一樣,是SingleChildRenderObjectWidget
。SizeBox.expand
的方法,就是提供一個儘可能大的元件。
使用
-
pinned
為true
因為我們的頭部是最後還是粘性在上面的,所以設定SliverPersistentHeader的pined為true
單頁內滑動時的動畫效果
- 不同
- 這個動畫效果和我們之前的動畫效果都不同,這意味著我們需要自定義動畫。
- 而它和我們上面兩個自定義的元件也不同,他是一個元件內包括了多個子元件。我們需要在約束變化的過程中,控制多組控制元件一起變化。
CustomMultiChildLayout
這個Widget可以完全自己掌控佈局的排列。我們需要做的是將它的自元件都傳遞給他,然後實現它的方法,就可以完全的掌握自己的佈局了。
完全符合我們的需求。
使用關鍵點
- 自定義MultiChildLayoutDelegate來自己實現佈局
- 他的每個child都需要用layoutId來包裹,並且分配給他們的id,都必須是唯一的。
分析動畫效果
包括的子元件
我們在這個元件中要安排動畫包括 4組SectionCard
、SectionTitle
、SectionIndicator
。
動畫的過程
-
開始狀態
開始狀態時,SectionCard
就是按照column來排列,平均分配螢幕的高度。SectionTitle
則是出現在每個SectionCard
的中間。SectionIndicator
位於右下角。 -
結束狀態
結束狀態時,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就可以實現了。
同時上下選中的狀態同步
可以看到無論是上面的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來監聽事件。之前介紹過。這是常規操作。 處理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的值,就會直接出發_AllSectionsView
rebuild。來完成動畫效果。
滾動時的物理效果
ScrollPhysics
這些滾動元件的物理滾動效果都是通過ScrollPhysics
來進行配置的。
Flutter自帶的
自動的ScrollPhysics
就有4個。
-
BouncingScrollPhysics
,彈性的滾動效果。 -
ClampingScrollPhysics
,正常的滾動效果,沒有彈性。 - NeverScrollableScrollPhysics,不滾動。
-
AlwaysScrollableScrollPhysics
,在Android上和ClampingScrollPhysics
一樣,在IOS上和BouncingScrollPhysics
一樣。
動畫分析
這個動畫中,有兩種處理。
PageView
因為上下都是PageView,當單頁內的動畫在初始狀態和結束狀態(中間)中間。是不能切換PageView的。當高度小於時,才能切換。
-
監聽滑動的距離
因為要監聽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
回顧使用以來的控制元件
同時,我們也可以進一步瞭解下面張圖的意思
- 組合型的控制元件
就是我們最常用的控制元件。 - 代理型的控制元件
這類控制元件,我們在入門的第二遍文章,就介紹過。用它來儲存狀態的。 - 展示型
展示型,我們這邊文章裡面遇到了。其實就是可以自己去繼承定義這樣的控制元件,完全控制的佈局規則和繪製規則。
最後
介紹到這邊文章,我們已經大體對Flutter的介面開發有了一個相對全面的瞭解。
後續,我們會繼續從幾個深入去探究。
- 一個是它的原始碼實現,看看他到底是怎麼實現的。
- 另個就是會去搭建真實的專案,看看如何寫一個Reactive 的Flutter專案。
- 混合工程的編譯流程
參考
Flutter SDK doc
Flutter中的佈局繪製流程簡析(一)
深入瞭解Flutter介面開發(強烈推薦)
我的部落格即將搬運同步至騰訊雲+社群,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1psdgylx8dufa
相關文章
- Flutter開發實戰分析-pesto_demo解析Flutter
- Flutter完整開發實戰詳解(二、快速開發實戰篇)Flutter
- Flutter完整開發實戰詳解(二十一、 Flutter 畫面渲染的全面解析)Flutter
- 【Flutter】開發之實戰Widget(四)Flutter
- 讀《python 高效開發實戰》Python
- Flutter完整開發實戰詳解(二、 快速開發實戰篇) | 掘金技術徵文Flutter
- 現有Android專案中整合Flutter/Flutter混合開發實戰(二):FlutterActivity原始碼分析AndroidFlutter原始碼
- Flutter實戰之開發問題集(一)Flutter
- Flutter 開發實戰——自定義省略字尾Flutter
- 承載億級流量的開發框架,閒魚 Flutter 技術解析與實戰大公開框架Flutter
- 重磅首發 | 承載億級流量的開發框架,閒魚 Flutter 技術解析與實戰大公開框架Flutter
- Flutter開發實戰 高仿微信(1)首頁Flutter
- Flutter開發實戰初級(一)ListView詳解FlutterView
- Flutter完整開發實戰詳解(五、 深入探索)Flutter
- Flutter開發之JSON解析FlutterJSON
- Flutter入門與實戰(五十一):Flutter多版本切換開發Flutter
- iphone app開發實戰很重要:《iPhone App開發實戰手冊》讀後感iPhoneAPP
- Flutter完整開發實戰詳解(十六、詳解自定義佈局實戰)Flutter
- Flutter開發之路由與導航Flutter路由
- Flutter開發實戰初級(一)ListView詳解2FlutterView
- Flutter完整開發實戰詳解(十四、混合開發打包 Android 篇)FlutterAndroid
- 現有Android專案中整合Flutter/Flutter混合開發實戰(一)AndroidFlutter
- iOS專案開發實戰——plist陣列解析iOS陣列
- Flutter - 實戰指導,使用ScopedModel管理狀態Flutter
- flutter開發之Dart[必讀篇]?FlutterDart
- Flutter完整開發實戰詳解(九、 深入繪製原理)Flutter
- Flutter完整開發實戰詳解(七、 深入佈局原理)Flutter
- Flutter完整開發實戰詳解(六、 深入Widget原理)Flutter
- Flutter完整開發實戰詳解(八、 實用技巧與填坑)Flutter
- Flutter開發之導航與路由管理Flutter路由
- 《Flutter實戰》開源電子書Flutter
- Flutter 混合開發實戰問題記錄(一)FlutterView閃爍FlutterView
- Flutter完整開發實戰詳解(十一、全面深入理解Stream)Flutter
- flutter packages 開發實戰——釋出失敗問題解決FlutterPackage
- Flutter完整開發實戰詳解(三、打包與填坑篇)Flutter
- Flutter 混合開發實戰問題記錄(三)打包並上傳flutter aar 到mavenFlutterMaven
- Flutter實戰圖片元件演進之外接紋理解析Flutter元件
- Flutter完整開發實戰詳解(十、 深入圖片載入流程)Flutter