接上回:git
pageController源碼分析github
此次記錄一下pageView的拆解過程,其中沒有太大關係的變量和方法會被忽略掉,還有一些在pageController 源碼分析這篇文章中有介紹過的,我會標註。bash
咱們先看構造函數:
(它有三個構造函數,咱們以PageView爲入口)
複製代碼
PageView({
Key key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PageController controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
})
複製代碼
結構圖:
複製代碼
其中controller、physics能夠參見 pageController源碼分析app
DragStartBehavior 這個參數須要講一下。less
DragStartBehavior 是一個枚舉類,代碼以下:ide
enum DragStartBehavior {
down,
start,
}
複製代碼
註釋是這樣說的:配置傳給DragStartDetails的offset(位置)。 DragStartDetails 在一些手勢回調、通知裏常常能夠看到。函數
通過進一步查找,在monodrag.dart中有這樣一段註釋:源碼分析
/// Configure the behavior of offsets sent to [onStart].
///
/// If set to [DragStartBehavior.start], the [onStart] callback will be called
/// at the time and position when this gesture recognizer wins the arena. If
/// [DragStartBehavior.down], [onStart] will be called at the time and
/// position when a down event was first detected.
複製代碼
大體意思是: 配置"位置"(例如你的手勢觸發的)傳給回調onStart的行爲。post
若是設置爲.start時,當手勢識別器在競技場勝出時纔會把對應的位置和時間傳給onStart回調。ui
若是設置爲.down,傳給onStart的時間和位置是 第一次檢測到事件的時候。
例如:
手指按在屏幕上時,位置爲(500,500),在贏得競技場前移動到了(510,500)。 這時咱們行爲採用DragStartBehavior.down,那麼onStart回調收到的offset是(500,500)。 而若是採用的是DragStartBehavior.start,那麼onStart回調收到的offset是(510,500)。
手勢識別器:如我們設置在GestureDector中的各類回調:tap,longPress,水平/垂直滑動等等
複製代碼
競技場又是什麼呢?
我在註釋中有這樣一個連接:
https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation
你可能須要fq,原文以下:
複製代碼
用俺蹩腳的英文翻一哈,有錯的還請指正。
釋義:
屏幕上某一個位置可能有多個手勢識別器。全部這些識別器都監聽來自stream所流出的指針事件,並識別它們所須要的手勢。具體識別哪些手勢,這個由GestureDector 這個widget中不爲空的回調來決定。
當用戶手指在屏幕上的一個位置觸發事件,而同時有多個識別器能夠匹配到這個事件時,那麼framework disambiguates會讓這些事件進入競技場,勝出的規則以下:
· 任什麼時候候,競技場上只有一個手勢識別器時,那麼這個識別器就算勝出。
· 任什麼時候候,由於某一因素致使其中一個識別器勝出,那麼剩餘的識別器全算輸。
舉個栗子,在水平和垂直拖動的消歧時,一旦按下事件出現(這裏預設水平和垂直識別器都能收到事件),兩個識別器都進入競技場。以後這倆識別器按兵不動,繼續觀察後續事件(移動),若是用戶水平移動了一段距離(邏輯像素),那麼水平識別器宣佈勝出,後續手勢會被看作水平手勢(horizontal gesture),垂直同理。
而對於只設置了一個手勢識別器,例如水平(垂直)識別器,競技場依然是很是有效的。假設,當競技場中只有一個水平識別器,那麼當用戶第一次接觸屏幕時,觸摸點的像素會被當作水平拖動來對待,而不用用戶後續的操做再去斷定。
至此,pageView就講完了,由於pageview是Statefulwidget,咱們接着看它的state
複製代碼
_PageViewState中的代碼很簡單,咱們直接看build方法,代碼以下:
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
final ScrollPhysics physics = _ForceImplicitScrollPhysics(
allowImplicitScrolling: widget.allowImplicitScrolling,
).applyTo(widget.pageSnapping
? _kPagePhysics.applyTo(widget.physics)
: widget.physics);
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 && widget.onPageChanged != null && notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics as PageMetrics;
final int currentPage = metrics.page.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged(currentPage);
}
}
return false;
},
child: Scrollable(
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: widget.controller,
physics: physics,
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
// TODO(dnfield): we should provide a way to set cacheExtent
// independent of implicit scrolling:
// https://github.com/flutter/flutter/issues/45632
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
cacheExtentStyle: CacheExtentStyle.viewport,
axisDirection: axisDirection,
offset: position,
slivers: <Widget>[
SliverFillViewport(
viewportFraction: widget.controller.viewportFraction,
delegate: widget.childrenDelegate,
),
],
);
},
),
);
}
複製代碼
一、final AxisDirection axisDirection = _getDirection(context); 獲得方向
二、定義物理效果,這個能夠參見Pagecontroller:juejin.im/post/5ef99d…
三、構建子widget樹,先是外面包了一層NotificationListener,用於根據子widget的滾動來算出當前在多少頁(page)。子widget是一個Scrollable
Scrollable建立一個滾動的wiget,參數跟pageview幾乎同樣,這裏再也不贅述。其自己是一個statefulWidget,並無child參數,而是viewportBuilder取而代之,參數也頗有意思,一個context和一個position。
咱們先看它的state,結構圖以下:
setCanDrag(bool),用於設置是否能夠拖動,若是能夠的話,就進一步生成識別器(水平/垂直)
_updatePosition(),這個方法看了前一篇文章的應該有印象,具體參見:
複製代碼
接下來是build()方法,源碼以下:
複製代碼
// DESCRIPTION
@override
Widget build(BuildContext context) {
assert(position != null);
// _ScrollableScope must be placed above the BuildContext returned by notificationContext
// so that we can get this ScrollableState by doing the following:
//
// ScrollNotification notification;
// Scrollable.of(notification.context)
//
// Since notificationContext is pointing to _gestureDetectorKey.context, _ScrollableScope
// must be placed above the widget using it: RawGestureDetector
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),
),
),
),
),
);
//這段不用看
<!--if (!widget.excludeFromSemantics) {-->
<!-- result = _ScrollSemantics(-->
<!-- key: _scrollSemanticsKey,-->
<!-- child: result,-->
<!-- position: position,-->
<!-- allowImplicitScrolling: widget?.physics?.allowImplicitScrolling ?? _physics.allowImplicitScrolling,-->
<!-- semanticChildCount: widget.semanticChildCount,-->
<!-- );-->
<!--}-->
return _configuration.buildViewportChrome(context, result, widget.axisDirection);
}
複製代碼
這個build方法上面有一行:
// DESCRIPTION 描述
複製代碼
實際上,這個build方法內,也確實沒有構建新的子widget,只是用一些widget來對齊進行包裹,並return一個:
_configuration.buildViewportChrome(context, result, widget.axisDirection);
此方法主要是根據不一樣系統返回不一樣的效果。例如:安卓機,滾動到尾部後繼續滾動,會出現藍色的水印
複製代碼
我們來看_ScrollableScope
其繼承inheritWidget,另外多存儲一個position,之因此用_ScrollableScope對本身包裹緣由是Scollable中的一個靜態方法:
static Future<void> ensureVisible(
BuildContext context, {
double alignment = 0.0,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
}) {
final List<Future<void>> futures = <Future<void>>[];
ScrollableState scrollable = Scrollable.of(context);
while (scrollable != null) {
futures.add(scrollable.position.ensureVisible(
context.findRenderObject(),
alignment: alignment,
duration: duration,
curve: curve,
alignmentPolicy: alignmentPolicy,
));
context = scrollable.context;
scrollable = Scrollable.of(context);
}
if (futures.isEmpty || duration == Duration.zero)
return Future<void>.value();
if (futures.length == 1)
return futures.single;
return Future.wait<void>(futures).then<void>((List<void> _) => null);
}
複製代碼
能夠滾動到指定的context,內部會調用controller.position.ensureVisible 經過這個context,找到對應的renderObject並滾動到該位置。
實際上在全部有滾動組件的頁面,你調用這個靜態方法,並傳入目標item的context
,均可以滾過去。不過一些會回收child的,如listview,你可能就滾溝裏去了(報空)。
複製代碼
由於上面的功能設定,因此這個是靜態方法。如何拿到context對應的ScrollableState並取得其中的position(好調用它的方法ensureVisible)呢?咱們能夠經過.of(context),以下:
static ScrollableState of(BuildContext context) {
final _ScrollableScope widget = context.dependOnInheritedWidgetOfExactType<_ScrollableScope>();
return widget?.scrollable;
}
複製代碼
爲何要拿position(ScrollPosition),能夠參見pageController源碼分析
複製代碼
能夠看到 context.dependOnInheritedWidgetOfExactType返回了咱們想要的,可是前提是,返回的東西必需要繼承自InheritedWidget,這也就是爲何咱們上面要用_ScrollableScope來進行包裹了。
咱們回到 Scrollablestate中的build方法向下看,_ScrollableScope的child就相對簡單了,對父widget傳過來的的builder用Listener和RawGestureDetector,進行了包裹。
Listener能夠分發事件,結構圖以下:
RawGestureDetector則能夠幫助child識別指定的手勢(參數gestures),而這個手勢,是在上面的setCanDrag()方法中生成的。
ScrollableState就分解完了,接下來咱們看一下Scrollable的viewportBuilder,也就是上面咱們對它包了好幾層的東西。
這個方法會返回一個ViewPort,按個人理解給它起了個名字叫視窗。
代碼以下:
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
// TODO(dnfield): we should provide a way to set cacheExtent
// independent of implicit scrolling:
// https://github.com/flutter/flutter/issues/45632
cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0,
cacheExtentStyle: CacheExtentStyle.viewport,
axisDirection: axisDirection,
offset: position,
slivers: <Widget>[
SliverFillViewport(
viewportFraction: widget.controller.viewportFraction,
delegate: widget.childrenDelegate,
),
],
);
},
複製代碼
它的繼承關係是以下(上到下,子到父)
viewport
↓
MultiChildRenderObjectWidget
↓
RenderObjectWidget
↓
Widget
複製代碼
這裏說一下,咱們經常使用的statelessWidget和statefulWidget也是繼承自Widget。 RenderObjectWidget和MultiChildRenderObjectWidget內容過多不在這裏展開,有興趣的能夠去查閱相關資料。
簡單的介紹一下RenderObjectWidget:咱們知道RenderObject是直接用於渲染的和繪製的,而RenderObjectWidget則是這個渲染和繪製的配置信息,同時配置變動須要從新繪製時,會調用updateRenderObject()。 它的源碼:
abstract class RenderObjectWidget extends Widget {
const RenderObjectWidget({ Key key }) : super(key: key);
@override
RenderObjectElement createElement();
@protected
RenderObject createRenderObject(BuildContext context);
@protected
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
@protected
void didUnmountRenderObject(covariant RenderObject renderObject) { }
}
複製代碼
它也會建立element,總體看起和狀態widget很像,那爲何那麼這裏的viewPort要用RenderObjectWidget呢?
viewPort開頭有這樣一句話:
/// The viewport listens to the [offset], which means you do not need to
/// rebuild this widget when the [offset] changes.
複製代碼
換言之,它只是一個窗戶,你以前建立的children(slivers)在窗戶外面滾動,你透過窗戶來瀏覽(具體瀏覽哪一個跟上面傳進來的position(offset)有關),這個窗戶是不會變更的。所以直接使用RenderObjectWidget一步到位更爲精簡。
viewportBuilder(BuildContext context, ViewportOffset position) 再看這個方法就一目瞭然
複製代碼
Viewport的另一個參數slivers:
slivers: <Widget>[
SliverFillViewport(
viewportFraction: widget.controller.viewportFraction,
delegate: widget.childrenDelegate,
),
],
複製代碼
這裏是比較簡單的,之因此能夠傳了一個SliverFillViewport包裹你的children,只是爲了保證你的展現效果符合pageView:一個child(sliver)充滿一個視窗。
至此咱們整個pageview粗略剖析完了
文章比較長,謝謝你們觀看。如有錯誤的地方或者沒說明白的,還請指正,感謝。
複製代碼
俺的過往文章
複製代碼