引:之前項(xiàng)目遇到一個(gè)需求,需要兩個(gè)嵌套的TabBarView在內(nèi)部滑動(dòng)到底之后可以滑外部,抄了點(diǎn)代碼然后修修補(bǔ)補(bǔ)姑且算是完成了,有的地方也只是看著函數(shù)和類的命名半猜著改的,也不知道具體是哪里調(diào)了這個(gè)函數(shù),總之挺不滿意的。所以就想著去看一下flutter內(nèi)部滾動(dòng)的實(shí)現(xiàn),結(jié)果一看發(fā)現(xiàn)滾動(dòng)相關(guān)的東西是真的非常龐大……不過好消息是,比較龐雜的部分,大概率開發(fā)者也不會(huì)去接觸,剩下的一些,倒是可以一看,有所用處的。
class ListView extends BoxScrollView {
// 省略一萬個(gè)構(gòu)造函數(shù)
@override
Widget buildChildLayout(BuildContext context) {
if (itemExtent != null) {
return SliverFixedExtentList(
delegate: childrenDelegate,
itemExtent: itemExtent,
);
}
return SliverList(delegate: childrenDelegate);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DoubleProperty('itemExtent', itemExtent, defaultValue: null));
// ...
}
ListView里唯一做了override的函數(shù)是buildChildLayout,康康哪里用了這個(gè)函數(shù)
abstract class BoxScrollView extends ScrollView {
// ...
@override
List<Widget> buildSlivers(BuildContext context) {
Widget sliver = buildChildLayout(context);
// ...
}
// ...
}
只有這一個(gè)地方,直接看buildSlivers,發(fā)現(xiàn)在ScrollView里面,ScrollView是一個(gè)普通的StateLessWidget,buildSlivers用在了build函數(shù)里
abstract class ScrollView extends StatelessWidget {
// 這里是傳入 buildViewport 的 Widget
@protected
Widget buildViewport(
BuildContext context,
ViewportOffset offset,
AxisDirection axisDirection,
List<Widget> slivers,
) {
if (shrinkWrap) {
return ShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
);
}
return Viewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
cacheExtent: cacheExtent,
center: center,
anchor: anchor,
);
}
@override
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);
final ScrollController scrollController =
primary ? PrimaryScrollController.of(context) : controller;
final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
semanticChildCount: semanticChildCount,
// viewportBuilder 在內(nèi)層直接通過 widget.viewportBuilder(context, position) 作為child傳入了
viewportBuilder: (BuildContext context, ViewportOffset offset) {
// 這里調(diào)了通過 buildSlivers 得到的 slivers
return buildViewport(context, offset, axisDirection, slivers);
},
);
// ...
}
// ...
}
在ScrollView這一層可以看到,最終build出的內(nèi)容是一個(gè)Viewport 或者 ShrinkWrappingViewport,兩者都是一個(gè) MultiChildRenderObjectWidget,內(nèi)部的布局邏輯大部分也是比較類似的。同時(shí)我們發(fā)現(xiàn),平時(shí)常用的 'slivers' 就是用在了ViewPort里面。關(guān)于 ViewPort 內(nèi)部具體做了什么可以參考這篇文章:
https://segmentfault.com/a/1190000015086603
簡(jiǎn)單概括來說,Sliver類似一個(gè)允許用戶選擇“哪一部分可以被看到”的Flex組件,顯然,這個(gè)特性可以用來做滾動(dòng)頁面。同時(shí)可以發(fā)現(xiàn)flutter的滾動(dòng)區(qū)也是一個(gè)一個(gè)renderBox畫出來的,讓人有一種自己可以控制flutter所有行為的錯(cuò)覺(?)。
滾動(dòng)布局方面的東西,實(shí)際上都由Viewport處理了,剩下的無非就是兩件事:獲取用戶的手勢(shì)和處理手勢(shì)與布局之間的關(guān)系。繼續(xù)讀代碼,可以發(fā)現(xiàn)手勢(shì)部分也是被封成了一塊穩(wěn)定且基本不需要改動(dòng)的模塊。從ScrollView中可以發(fā)現(xiàn)viewportBuilder是在Scrollable里調(diào)的,而Scrollable內(nèi)部則是這樣:
@override
Widget build(BuildContext context) {
Widget result = _ScrollableScope(
scrollable: this,
position: position,
// TODO(ianh): Having all these global keys is sad.
child: Listener(
onPointerSignal: _receivedPointerSignal,
child: RawGestureDetector(
key: _gestureDetectorKey,
gestures: _gestureRecognizers,
behavior: HitTestBehavior.opaque,
excludeFromSemantics: widget.excludeFromSemantics,
child: Semantics(
explicitChildNodes: !widget.excludeFromSemantics,
child: IgnorePointer(
key: _ignorePointerKey,
ignoring: _shouldIgnorePointer,
ignoringSemantics: false,
child: widget.viewportBuilder(context, position),
),
),
),
),
);
Listener組件看起來很陌生,但是常用的GestureDetector組件其實(shí)就是Listener的簡(jiǎn)單包裝,Listener組件內(nèi)部對(duì)手勢(shì)做了很詳盡的處理,不太需要修改,(并且由于我不是很想讀這一塊),所以這里就不多關(guān)心。實(shí)際上我們更多關(guān)心的還是:手勢(shì)如何對(duì)Viewport的布局造成影響。于是我們先回到Viewport
@override
RenderViewport createRenderObject(BuildContext context) {
return RenderViewport(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
anchor: anchor,
offset: offset,
cacheExtent: cacheExtent,
cacheExtentStyle: cacheExtentStyle,
);
}
Viewport的createRenderObject如圖,可以看到這里能影響顯示區(qū)域的只有一個(gè)offset,offset是一個(gè)叫ViewportOffset的玩意,而繼續(xù)讀代碼可以發(fā)現(xiàn),有時(shí)候會(huì)見到的ScrollPosition就是ViewportOffset的子類(察覺)。
在ScrollPosition看它改寫了哪些方法,很容易發(fā)現(xiàn)最關(guān)鍵的地方:
@override
double get pixels => _pixels;
double _pixels;
其余的改寫都是一些最大滾動(dòng)區(qū),jumpTo之類的方法,并不是特別關(guān)鍵。所以要知道手勢(shì)何時(shí)影響了Viewport,最重要就是要知道手勢(shì)如何影響ScrollPosition中的pixels。
可以在Scrollable中看到,Listener給出的事件是在_gestureRecognizers中監(jiān)聽的,而_gestureRecognizers是在setCanDrag方法中注冊(cè)的
_gestureRecognizers = <Type, GestureRecognizerFactory>{
VerticalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<VerticalDragGestureRecognizer>(
() => VerticalDragGestureRecognizer(),
(VerticalDragGestureRecognizer instance) {
instance
..onDown = _handleDragDown
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd
..onCancel = _handleDragCancel
..minFlingDistance = _physics?.minFlingDistance
..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity
..dragStartBehavior = widget.dragStartBehavior;
},
),
};
尋找setCanDrag方法,發(fā)現(xiàn)追根溯源一大串,最終是在Viewport的layout函數(shù)里面間接調(diào)用了。好,喜大普奔,不是我們需要關(guān)心的事情了。來關(guān)心一下幾個(gè)handleDrag:
void _handleDragDown(DragDownDetails details) {
assert(_drag == null);
assert(_hold == null);
_hold = position.hold(_disposeHold);
}
void _handleDragUpdate(DragUpdateDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_drag?.update(details);
}
void _handleDragEnd(DragEndDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_drag?.end(details);
assert(_drag == null);
}
void _handleDragCancel() {
// _hold might be null if the drag started.
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_hold?.cancel();
_drag?.cancel();
assert(_hold == null);
assert(_drag == null);
}
于是我們發(fā)現(xiàn),實(shí)際上就是ScrollPosition在中間協(xié)調(diào)手勢(shì)和viewPort位置的關(guān)系?;氐轿恼麻_頭的問題,要自己控制滾動(dòng)行為,實(shí)際上就是需要有一個(gè)滿足需求的ScrollPosition來處理Drag和Hold方法。
當(dāng)然,之后還會(huì)有Physics給出的goBallistic等方法來處理用戶手勢(shì)結(jié)束后的滾動(dòng)動(dòng)作,這些就在之后的文章再談吧。