Flutter——PageView源碼和Gesture競技場消歧的淺析

前言

接上回:git

pageController源碼分析github

此次記錄一下pageView的拆解過程,其中沒有太大關係的變量和方法會被忽略掉,還有一些在pageController 源碼分析這篇文章中有介紹過的,我會標註。bash

PageView

咱們先看構造函數:
(它有三個構造函數,咱們以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

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

_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

Scrollable建立一個滾動的wiget,參數跟pageview幾乎同樣,這裏再也不贅述。其自己是一個statefulWidget,並無child參數,而是viewportBuilder取而代之,參數也頗有意思,一個context和一個position。

咱們先看它的state,結構圖以下:

setCanDrag(bool),用於設置是否能夠拖動,若是能夠的話,就進一步生成識別器(水平/垂直)

_updatePosition(),這個方法看了前一篇文章的應該有印象,具體參見:
複製代碼

PageController源碼解析

接下來是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

_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

Listener能夠分發事件,結構圖以下:

RawGestureDetector

RawGestureDetector則能夠幫助child識別指定的手勢(參數gestures),而這個手勢,是在上面的setCanDrag()方法中生成的。

ScrollableState就分解完了,接下來咱們看一下Scrollable的viewportBuilder,也就是上面咱們對它包了好幾層的東西。

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粗略剖析完了

文章比較長,謝謝你們觀看。如有錯誤的地方或者沒說明白的,還請指正,感謝。
複製代碼

關聯文章

pageController源碼分析

俺的過往文章
複製代碼

juejin.im/post/5edc91…

相關文章
相關標籤/搜索