Flutter 滑動體系

ScrollView

可滑動視圖的父類,ListView,CustomScrollView 和 GridView 都是它的子類,它們經過實現 buildSlivers 函數爲 ScrollView 提供子視圖,同時將 ScrollController,ScrollPhysics,ViewportBuilder 和 children 等傳遞給 Scrollable。linux

在 ScrollView 的 build 中,一些 ScrollView 的參數,如 dragStartBehavior,controller 以及 buildSlivers 這些函數,會用來生成一個 Scrollable,它對 ScrollView 的一些東西進行收攏(好比 ScrollView 不一樣子類的實現),而後專一於實現滑動這一功能。android

Viewport

Viewport 負責計算 ScrollView 的大小,通常有兩種,ShrinkWrappingViewport 和 Viewport,它們的區別在於 ScrollView 大小的計算方式,Viewport 在 performResize 階段就能夠肯定本身的大小,即父 widget 提供的最大空間,而 ShrinkWrappingViewport 要在 performLayout 階段才能肯定,由於它的大小依賴於本身的子 widget,須要先統計子 view 的大小,再肯定自身的大小。ios

因此,當咱們使用 ScrollView 時,通常咱們須要給它必定有限大小的 constraints,它才能正確計算本身的大小,當咱們沒法提供這樣一個環境,就能夠將它的 shrinkWrap 設置爲 true,這樣它會給本身計算一個合適的大小。spring

ScrollConfiguration

這是一個 InheritedWidget,它的做用是給 Scrollable 傳遞 ScrollBehavior,而它的肯定,在很早以前就肯定了,且通常一個 app 只有一個(本身單獨聲明使用的另算),好比在 _CupertinoAppState 中:windows

Widget build(BuildContext context) {
  final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
  return ScrollConfiguration(
    behavior: _AlwaysCupertinoScrollBehavior(),
    child: CupertinoUserInterfaceLevel(
      data: CupertinoUserInterfaceLevelData.base,
      child: CupertinoTheme(
        data: effectiveThemeData,
        child: HeroControllerScope(
          controller: _heroController,
          child: Builder(
            builder: _buildWidgetApp,
          ),
        ),
      ),
    ),
  );
}
複製代碼

給整個 app 指定了一個全局的 ScrollBehavior _AlwaysCupertinoScrollBehavior,這個會在 Scrollable 執行滑動的時候用到。markdown

ScrollBehavior

ScrollBehavior 自己在 flutter 的設計中是一個平臺相關的 Widget,它會根據當前的平臺,選擇一個合適的 ScrollPhysics,以下:app

ScrollPhysics getScrollPhysics(BuildContext context) {
  switch (getPlatform(context)) {
    case TargetPlatform.iOS:
    case TargetPlatform.macOS:
      return _bouncingPhysics;
    case TargetPlatform.android:
    case TargetPlatform.fuchsia:
    case TargetPlatform.linux:
    case TargetPlatform.windows:
      return _clampingPhysics;
  }
}
複製代碼

而 ScrollPhysics 的定位,能夠從名字上理解,控制滑動過程的物理特性,定義瞭如當滑動到頂部的時候的表現、滑過頭了以後的回彈方式等。less

Scrollable

ScrollView 中 build 主要返回的 widget,一個通用的滑動模型,它是滑動功能的載體,與 ScrollController、ScrollPhysics 一塊兒實現了一個可滑動的控件。而它也只算是一個載體,一箇中介,它最主要的做用,就是利用身爲 widget 的優點,從整個視圖體系中拿到觸摸事件,而剩下的功能,交給其餘人就好。ide

Viewport 負責決定滑動視圖的大小,ScrollPosition 決定滑動的位置,ScrollPhysics 決定滑動的物理屬性,ScrollController 能夠支持外部使用者控制滑動過程。還有其餘的一些,好比 ScrollActivity 表示了滑動過程當中的某一階段等。在這樣一個體系中,ScrollPosition 更像是一個 controller,它直接從 Scrollable 中拿到未處理的觸摸事件,根據事件類型計算出本身當前的狀態。函數

Scrollable 對應的 state 爲 ScrollableState,在它的 build 中,返回的 child 中,有 Listener、RawGestureDetector 和 Viewport 等,Listener 用於監聽 PointerScrollEvent 事件,通常這個事件應該是在滾動滑動條時觸發的,此時它會計算出一個滑動位置,並直接調用 ScrollPosition.jumpTo 滾動到對應位置。

而 RawGestureDetector 會監聽一些滑動手勢,好比 dragDown、dragStart 等,ScrollPosition 根據這些手勢信息更新狀態,計算滑動。

Viewport 就是在 ScrollView 中生成的。

ScrollController

ScrollController 能夠從 ScrollView 中設置,並一路傳遞到 Scrollable 中,它雖然名爲 controller,但並非一個 center controller,而是一個 user controller,即給用戶提供控制滑動狀態的一種途徑,但自己在滑動體系中做用不大,只是一個將用戶的命令傳達到 ScrollPosition 中的角色,不過它還有一點權利,就是可能決定建立的 ScrollPosition,固然這個最終仍是將權利傳遞到外部用戶的手上而已。

它有諸如 adjumpTo、animateTo 等函數,經過調用 position 的同名函數實現。另外,ScrollController 能夠綁定多個 ScrollPosition,能夠據此實現多個視圖同步滑動的能力。

ScrollPosition

ScrollPosition 承擔着滑動過程當中的主要責任,上承 ScrollController,Scrollable,下啓 ScrollActivity、ScrollPhysics 等。

首先,ScrollPosition 是 ViewportOffset 的一個子類,這是一個 widget 向概念,它表示的是 Viewport 這個 widget 的偏移量,因爲 Viewport 自己就是用於承載滑動視圖的 widget,在不少狀況下,它的 children 的總體長度要大於它自身,因此就須要有一個 offset 屬性,控制當前應該顯示的內容。另外,ViewportOffset 中也經過 applyViewportDimension 等函數,接收來自 widget 的信息,及時根據當前的佈局,更改顯示內容。

其次,ScrollPosition 做爲接收觸摸事件者,它還完成了對觸摸事件的分發功能,以及進一步,將處理過的觸摸事件轉換成視圖滑動(其中有一些複雜操做,好比須要考慮視圖滑動範圍、滑動物理屬性等),最終視圖更新。

舉個例子,當一個滑動事件發生時,它會建立一個Drag 處理後續的滑動事件,Drag 後續對原始的滑動事件進行第一次加工以後,再給到 ScrollPosition,而後 ScrollPosition 還會再將這個數據拿給 ScrollPhysics 進行一些相似邊界問題的判斷,完了以後,將最終結果給到 ViewportOffset 的 pixels 屬性,最後通知 Viewport 進行從新 layout,由此完成一次滑動。更具體的流程,在最後詳細說明。

ScrollPyhsics

ScrollPhysic 描述的是一個滑動視圖,也就是 Viewport 的內容,在執行滑動過程當中的一些物理屬性,好比是否能夠 overscroll,在一個給定的 ScrollMetrics 和理論偏移值下計算一個實際的偏移值等。再看下它的一些成員變量:

  • spring,SpringDescription,描述了滑動的一些物理特性,會在建立 Simulation 時傳遞過去

  • tolerance,Tolerance,定義了一些可忽略的距離、速度、時間等

  • flingDistance,定義了最小的可被認定爲 fling 手勢的距離

  • flingVelocity,定義了最小的可被認定爲 fling 手勢的速度,和最大的 fling 速度

  • dragStartDistanceMotionThreshold,定義了開始滑動時,可被認定爲是滑動手勢的最小距離

  • allowImplicitScrolling,這是一個來自 ViewportOffset 的變量

ScrollActivity

ScrollActivity 能夠表示滑動過程當中的一個階段,只是記錄了當前的狀態,好比是不是滑動中、當前的滑動速度等。它的幾個基本參數分別爲:

  • delegate,ScrollActivityDelegate,有着更新滑動位置的實現,通常就是 ScrollPosition 及其子類
  • shouldIgnorePointer,是否忽略觸摸事件,這裏的主體,是 Scrollable 的子 widget,也就是 Viewport,而在 Scrollable 中用於接收手勢滑動的 RawGestureDetector 在它之上,也就是說,這個參數並非控制是否檢測滑動手勢,而是待滑動的內容是否能夠接收事件,因此,在衆多 ScrollActivity 中,只有 HoldScrollActivity 和 IdleScrollActivity 中它的值才爲 true
  • isScrolling,當前是否處於滑動狀態
  • velocity,若是是滑動狀態,當前的滑動速度,固然,這個值也只有在 BallisticScrollActivity 和 DrivenScrollActivity 中才不爲 0

它大體能夠分爲兩種類型,滑動和不滑動。

non-scroll

其中表示不滑動的有兩個 ScrollActivity,HoldScrollActivity 和 IdleScrollActivity。

HoldScrollActivity

HoldScrollActivity 會在手指按下的瞬間生成,它有表示一種蓄勢待發的狀態,是爲了下一刻的滑動,因此在啓動 HoldScrollActivity 的時候,會保存下來當前的滑動速度,而後在開始滑動時,會在一個初始速度上接着滑動。

ScrollHoldController hold(VoidCallback holdCancelCallback) {
  final double previousVelocity = activity!.velocity;
  // ...
}
複製代碼
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
  final ScrollDragController drag = ScrollDragController(
    delegate: this,
    details: details,
    onDragCanceled: dragCancelCallback,
    carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity),
    motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold,
  );
  // ...
}
複製代碼
IdleScrollActivity

而 IdleScrollActivity 只是表示這是一個靜止狀態,此時 ScrollPosition 不進行滑動,也基本不處理事件。不過換句話說,ScrollPosition 也只是處理兩種事件,在 dragDown 時將狀態切換至 HoldScrollActivity,當 dargStart 時,生成 Drag 並將狀態切換至 DragScrollActivity,至於 dragUpdate 事件,則是直接交給 Drag 來處理的。

scroll

表示滑動狀態的 ScrollActivity 有三種,分別是事件驅動、速度驅動和動畫驅動。

driven by draging - DragScrollActivity

所謂事件驅動,就是滑動過程是根據外部傳進來的滑動事件,來決定是否以及如何更新視圖。這個就是在基本的滑動過程當中,ScrollPosition 接收到 dragStart 事件時,進入的滑動狀態,與之關聯的 ScrollDragController,它會被傳遞迴 Scrollable,在 dragUpdate 事件到來時直接處理事件。

void _handleDragStart(DragStartDetails details) {
  // It's possible for _hold to become null between _handleDragDown and
  // _handleDragStart, for example if some user code calls jumpTo or otherwise
  // triggers a new activity to begin.
  assert(_drag == null);
  _drag = position.drag(details, _disposeDrag);
  assert(_drag != null);
  assert(_hold == null);
}
複製代碼
void _handleDragUpdate(DragUpdateDetails details) {
  // _drag might be null if the drag activity ended and called _disposeDrag.
  assert(_hold == null || _drag == null);
  _drag?.update(details);
}
複製代碼
driven by velocity - BallisticScrollActivity

當 drag 系列事件結束後,會留下一個滑動速度,此時滑動並不會中止,而是在基於這個速度下,作減速滑動,直到速度爲 0,或者滑動到邊界,這個階段,對應的就是 BallisticScrollActivity。

deiven by animation - DrivenScrollActivity

當咱們直接經過 ScrollController 控制 Scrollable 進行滑動時,通常就是調用 animateTo,會建立一個 DrivenScrollActivity,根據當前給出的 duration、curve 等,建立一個動畫並執行。

Simulation

在 BallisticScrollActivity 執行過程當中,用於決定滑動位置的就是 Simulation,

void goBallistic(double velocity) {
  assert(hasPixels);
  final Simulation? simulation = physics.createBallisticSimulation(this, velocity);
  if (simulation != null) {
    beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
  } else {
    goIdle();
  }
}
複製代碼

Simulation 由 ScrollPhysics 建立,在必定程度上是平臺相關的,自己也算是 ScrollPhysics 功能組成的一部分,主要是用於控制拖拽滑動結束後的過程,好比在 ios 中默認使用的 BouncingScrollPhysics 會建立一個 BouncingScrollSimulation,建立 BouncingScrollSimulation 的時候,給了它一個初速度、滑動範圍等,而後就由它來肯定滑動的距離以及中止的時間。

完整的滑動過程

下面從一次完整的滑動過程再次分析下 flutter 中 Scrollable 的滑動體系,以 ScrollPositionWithSingleContext 和 BouncingScrollPhysics 爲例。

drag

首先,當 Scrollable 建立完成以後,它會利用 RawGestureDetector 監聽當前的手勢操做,主要監聽的操做就是 drag 事件相關的,好比 dragDown、dragStart、dragUpdat、dragCancel 等,在這些過程當中,主要涉及的只有 DragScrollActivity 這個。

onDown

onDown 用於處理 dragDown 事件,

void _handleDragDown(DragDownDetails details) {
  assert(_drag == null);
  assert(_hold == null);
  _hold = position.hold(_disposeHold);
}
複製代碼

很簡單,這裏只是調用 ScrollPosition 的 hold,建立一個 HoldScrollActivity,如上所介紹的,爲下一步的滑動做準備。這裏有一點,就是在建立 HoldScrollActivity 的時候,同時傳進去了一個 dispose 回調,在這個回調中,會將 _hold 置空,固然這裏的考慮並非釋放空間這麼簡單,_hold 自己仍是一種狀態,當它不爲空的時候,就意味着當前處於 HoldScrollActivity 所管轄的狀態,_drag 也是同理,會有 assert 對當前的狀態進行判斷。

onStart

onStart 處理 dragStart 事件,

void _handleDragStart(DragStartDetails details) {
  // It's possible for _hold to become null between _handleDragDown and
  // _handleDragStart, for example if some user code calls jumpTo or otherwise
  // triggers a new activity to begin.
  assert(_drag == null);
  _drag = position.drag(details, _disposeDrag);
  assert(_drag != null);
  assert(_hold == null);
}
複製代碼

跟 onDown 相似,此時建立了 _drag,在 position.drag 中,

Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
  final ScrollDragController drag = ScrollDragController(
    delegate: this,
    details: details,
    onDragCanceled: dragCancelCallback,
    carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity),
    motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold,
  );
  beginActivity(DragScrollActivity(this, drag));
  assert(_currentDrag == null);
  _currentDrag = drag;
  return drag;
}
複製代碼

ScrollDragController 會被建立並進入到 DragScrollActivity 狀態。在 ScrollPosition 中,每次啓用一個 ScrollActivity 時,都是使用 beginActivity 進行狀態切換的。

void beginActivity(ScrollActivity? newActivity) {
  if (newActivity == null)
    return;
  bool wasScrolling, oldIgnorePointer;
  if (_activity != null) {
    oldIgnorePointer = _activity!.shouldIgnorePointer;
    wasScrolling = _activity!.isScrolling;
    if (wasScrolling && !newActivity.isScrolling)
      didEndScroll(); // notifies and then saves the scroll offset
    _activity!.dispose();
  } else {
    oldIgnorePointer = false;
    wasScrolling = false;
  }
  _activity = newActivity;
  if (oldIgnorePointer != activity!.shouldIgnorePointer)
    context.setIgnorePointer(activity!.shouldIgnorePointer);
  isScrollingNotifier.value = activity!.isScrolling;
  if (!wasScrolling && _activity!.isScrolling)
    didStartScroll();
}
複製代碼

一個比較常規的切換邏輯,有一點就是,在 beginActivity 執行時,會判斷一下切換先後的滑動狀態和是否能夠接收事件,併產生相應的通知。

好比 didStartScroll,

void didStartScroll() {
  activity!.dispatchScrollStartNotification(copyWith(), context.notificationContext);
}

void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext? context) {
  ScrollStartNotification(metrics: metrics, context: context).dispatch(context);
}

void dispatch(BuildContext? target) {
  // The `target` may be null if the subtree the notification is supposed to be
  // dispatched in is in the process of being disposed.
  target?.visitAncestorElements(visitAncestor);
}

bool visitAncestor(Element element) {
  if (element is StatelessElement) {
    final StatelessWidget widget = element.widget;
    if (widget is NotificationListener<Notification>) {
      if (widget._dispatch(this, element)) // that function checks the type dynamically
        return false;
    }
  }
  return true;
}
複製代碼

這個過程會建立一個 ScrollStartNotification,並沿着 widget 樹向上傳遞,經過 visitAncestor,傳遞給上層第一個接收消費掉此通知的 NotificationListener。(只有當 NotificationListener 的 NotificationListenerCallback 返回 true 纔是消費此通知,不然通知會一直向上傳遞)

onUpdate

onUpdate 會處理 dragUpdate 事件,也就是手指滑動時的事件,此時這個事件會直接交給 ScrollDragController 處理,

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 update(DragUpdateDetails details) {
  assert(details.primaryDelta != null);
  _lastDetails = details;
  double offset = details.primaryDelta!;
  if (offset != 0.0) {
    _lastNonStationaryTimestamp = details.sourceTimeStamp;
  }
  // By default, iOS platforms carries momentum and has a start threshold
  // (configured in [BouncingScrollPhysics]). The 2 operations below are
  // no-ops on Android.
  _maybeLoseMomentum(offset, details.sourceTimeStamp);
  offset = _adjustForScrollStartThreshold(offset, details.sourceTimeStamp);
  if (offset == 0.0) {
    return;
  }
  if (_reversed) // e.g. an AxisDirection.up scrollable
    offset = -offset;
  delegate.applyUserOffset(offset);
}
複製代碼

在 update 中,保存下來的 _lastDetails 是爲了在以後發送通知的時候,加上這個滑動事件信息,好比 dispatchScrollUpdateNotification,

void dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) {
  final dynamic lastDetails = _controller!.lastDetails;
  assert(lastDetails is DragUpdateDetails);
  ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta, dragDetails: lastDetails as DragUpdateDetails).dispatch(context);
}
複製代碼

而後就是一個關因而否損失動量的判斷,

void _maybeLoseMomentum(double offset, Duration? timestamp) {
  if (_retainMomentum &&
      offset == 0.0 &&
      (timestamp == null || // If drag event has no timestamp, we lose momentum.
       timestamp - _lastNonStationaryTimestamp! > momentumRetainStationaryDurationThreshold)) {
    // If pointer is stationary for too long, we lose momentum.
    _retainMomentum = false;
  }
}
複製代碼

這個過程的目的,是爲了判斷是否損失動量,咱們知道,通常在 ios 的滑動中,連續快速滑動的時候,速度是會積累的,因此後面會越滑越快,而 flutter 爲了保持這一特性,就有了動量積累這樣一個功能,目前也只在 BouncingScrollPhysics 中才有。關於這個就要從 HoldScrollActivity 開始提及,以前 HoldScrollActivity 有提到,當 dragDown 事件發生時,ScrollPosition 會記錄下當前的滑動速度(若是當前還在滑動中),而後在 dragStart 時,將以前的滑動速度傳遞給 ScrollDragController,不過須要通過 ScrollPhysics 再過濾,而只有 BouncingScrollPhysics 纔會提供這個初速度,不過也是通過計算的:

double carriedMomentum(double existingVelocity) {
  return existingVelocity.sign *
      math.min(0.000816 * math.pow(existingVelocity.abs(), 1.967).toDouble(), 40000.0);
}
複製代碼

而後就是在 ScrollDragController 結束時,它會在滑動速度的基礎上,再把初速度加上去,構成滑動後的速度。而在滑動後是否加上初速度也是須要判斷的,就是經過 _maybeLoseMomentum,若是是滑動太慢或者有懸停的話,就認爲這不能進行動量積累,也就不會把初速度再加上去。

而後,_adjustForScrollStartThreshold 會就開始滑動的距離作些處理,大致就是,當本次滑動距離超過某個閾值的時候,才真正開始滑動,不然就看成偏差忽略掉。固然這個邏輯也是能夠經過 ScrollPhysics 控制的,就是它的 dragStartDistanceMotionThreshold,目前也只是 BouncingScrollPhysics 纔有。

double _adjustForScrollStartThreshold(double offset, Duration? timestamp) {
  if (timestamp == null) {
    // If we can't track time, we can't apply thresholds.
    // May be null for proxied drags like via accessibility.
    return offset;
  }
  if (offset == 0.0) {
    if (motionStartDistanceThreshold != null &&
        _offsetSinceLastStop == null &&
        timestamp - _lastNonStationaryTimestamp! > motionStoppedDurationThreshold) {
      // Enforce a new threshold.
      _offsetSinceLastStop = 0.0;
    }
    // Not moving can't break threshold.
    return 0.0;
  } else {
    if (_offsetSinceLastStop == null) {
      // Already in motion or no threshold behavior configured such as for
      // Android. Allow transparent offset transmission.
      return offset;
    } else {
      _offsetSinceLastStop = _offsetSinceLastStop! + offset;
      if (_offsetSinceLastStop!.abs() > motionStartDistanceThreshold!) {
        // Threshold broken.
        _offsetSinceLastStop = null;
        if (offset.abs() > _bigThresholdBreakDistance) {
          // This is heuristically a very deliberate fling. Leave the motion
          // unaffected.
          return offset;
        } else {
          // This is a normal speed threshold break.
          return math.min(
            // Ease into the motion when the threshold is initially broken
            // to avoid a visible jump.
            motionStartDistanceThreshold! / 3.0,
            offset.abs(),
          ) * offset.sign;
        }
      } else {
        return 0.0;
      }
    }
  }
}
複製代碼

在這個函數中,大致分爲了幾種判斷標準:

  • 有沒有時間信息
  • 滑動距離爲不爲 0
  • 是否有 motionStartDistanceThreshold

當沒有時間信息和 motionStartDistanceThreshold 沒有值的時候,這個函數能夠認爲不工做狀態,都是直接返回原滑動距離就完了。主要仍是看 motionStartDistanceThreshold 不爲空的狀況。

首先,當已經開始滑動,但滑動過程當中有懸停時,_offsetSinceLastStop 會歸零,從新開始計算。當開始滑動時,會逐漸積累 _offsetSinceLastStop,這個過程當中不會有實際滑動,直到它大於 motionStartDistanceThreshold 時,閾值到達,此時 _offsetSinceLastStop 置空,開始實際滑動。

不過這還只是從 ScrollDragController 的角度,認爲能夠滑動的距離,但真正反饋到 Viewport 以前,ScrollPhysics 也要來表現一下,

void applyUserOffset(double delta) {
  updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
  setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));
}
複製代碼

首先就是 applyPhysicsToUserOffset,也是隻有 BouncingScrollPhysics 有實現,緣由是由於它是一個容許 overscroll 的 ScrollPhysics,在這個函數中,主要是就 overscroll 的狀況,經過改變實際移動的距離,添加一種相似「阻力」的概念,即在 overscroll 狀態下,實際的滑動距離要小於手勢滑動距離。

double setPixels(double newPixels) {
  if (newPixels != pixels) {
    final double overscroll = applyBoundaryConditions(newPixels);
    final double oldPixels = pixels;
    _pixels = newPixels - overscroll;
    if (_pixels != oldPixels) {
      notifyListeners();
      didUpdateScrollPositionBy(pixels - oldPixels);
    }
    if (overscroll != 0.0) {
      didOverscrollBy(overscroll);
      return overscroll;
    }
  }
  return 0.0;
}

double applyBoundaryConditions(double value) {
  final double result = physics.applyBoundaryConditions(this, value);
  return result;
}
複製代碼

而後在 setPixels 中,繼續調用 ScrollPhysics 的 applyBoundaryConditions 判斷當前的 overscroll,固然這裏的 overscroll 與上面 applyPhysicsToUserOffset 中所說的不太同樣,上面說的 overscroll 是指用戶所觀察到的,若是要用語言簡單區分,它們能夠是:

  1. 在 applyPhysicsToUserOffset 中:用戶所觀察到的 overscroll,即須要展現給用戶的
  2. 在 applyBoundaryConditions 中:ScrollPosition 所觀察到的 overscroll,是不能讓用戶看到的

因此能夠看到,只有 ClampingScrollPhysics 對其有實現,而由於 BouncingScrollPhysics 是始終能夠滑動的狀態(經過阻力表達滑動到邊界),因此它在這裏的 overscroll 是始終爲 0。

完了以後才能獲得真正的須要偏移的數值,此時一次 dragUpdate 完成。

onEnd

接下來就是 dragEnd,滑動手勢結束的時候觸發。

void end(DragEndDetails details) {
  assert(details.primaryVelocity != null);
  // We negate the velocity here because if the touch is moving downwards,
  // the scroll has to move upwards. It's the same reason that update()
  // above negates the delta before applying it to the scroll offset.
  double velocity = -details.primaryVelocity!;
  if (_reversed) // e.g. an AxisDirection.up scrollable
    velocity = -velocity;
  _lastDetails = details;
  // Build momentum only if dragging in the same direction.
  if (_retainMomentum && velocity.sign == carriedVelocity!.sign)
    velocity += carriedVelocity!;
  delegate.goBallistic(velocity);
}
複製代碼

此時就是計算一下當前的滑動速度,以便後面進入 BallisticScrollActivity 階段,也就是 goBallistic 調用。

void goBallistic(double velocity) {
  assert(hasPixels);
  final Simulation? simulation = physics.createBallisticSimulation(this, velocity);
  if (simulation != null) {
    beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
  } else {
    goIdle();
  }
}

BallisticScrollActivity(
  ScrollActivityDelegate delegate,
  Simulation simulation,
  TickerProvider vsync,
) : super(delegate) {
  _controller = AnimationController.unbounded(
    debugLabel: kDebugMode ? objectRuntimeType(this, 'BallisticScrollActivity') : null,
    vsync: vsync,
  )
    ..addListener(_tick)
    ..animateWith(simulation)
     .whenComplete(_end); // won't trigger if we dispose _controller first
}
複製代碼

函數自己很簡單,首先經過 ScrollPhysics 建立一個 Simulation,而後將其傳給 BallisticScrollActivity。從 BallisticScrollActivity 的構造函數能夠看到,本質上咱們也能夠將其看做是一個由動畫驅動的滑動過程,只不過這個動畫是根據一個給定的初始速度建立的。

BallisticScrollActivity 與 DrivenScrollActivity 的類似度很高,它們都是在構造函數中先根據提供的信息(simulation,duration、curve等)建立一個 AnimationController,而後監聽更新和結束事件,在 _tick 中更新偏移值,在 _end 中結束本身。

ballistic

當滑動手勢結束時,遠不意味着整個滑動的結束,爲了用戶體驗,咱們賦予滑動速度的概念,那它的滑動也就有動量,因此中止不能只是戛然而止,須要一個慢慢停下來的過程,因此就有了 BallisticScrollActivity 所表明的減速過程,而這個過程的主要控制者,實際爲 ScrollPhysics 所生成的 Simulation,不一樣的 Simulation 相距甚遠。這裏就以較爲複雜的 BouncingScrollSimulation 爲例說明。

BouncingScrollSimulation({
  required double position,
  required double velocity,
  required this.leadingExtent,
  required this.trailingExtent,
  required this.spring,
  Tolerance tolerance = Tolerance.defaultTolerance,
}) : assert(position != null),
     assert(velocity != null),
     assert(leadingExtent != null),
     assert(trailingExtent != null),
     assert(leadingExtent <= trailingExtent),
     assert(spring != null),
     super(tolerance: tolerance) {
  if (position < leadingExtent) {
    _springSimulation = _underscrollSimulation(position, velocity);
    _springTime = double.negativeInfinity;
  } else if (position > trailingExtent) {
    _springSimulation = _overscrollSimulation(position, velocity);
    _springTime = double.negativeInfinity;
  } else {
    // Taken from UIScrollView.decelerationRate (.normal = 0.998)
    // 0.998^1000 = ~0.135
    _frictionSimulation = FrictionSimulation(0.135, position, velocity);
    final double finalX = _frictionSimulation.finalX;
    if (velocity > 0.0 && finalX > trailingExtent) {
      _springTime = _frictionSimulation.timeAtX(trailingExtent);
      _springSimulation = _overscrollSimulation(
        trailingExtent,
        math.min(_frictionSimulation.dx(_springTime), maxSpringTransferVelocity),
      );
      assert(_springTime.isFinite);
    } else if (velocity < 0.0 && finalX < leadingExtent) {
      _springTime = _frictionSimulation.timeAtX(leadingExtent);
      _springSimulation = _underscrollSimulation(
        leadingExtent,
        math.min(_frictionSimulation.dx(_springTime), maxSpringTransferVelocity),
      );
      assert(_springTime.isFinite);
    } else {
      _springTime = double.infinity;
    }
  }
  assert(_springTime != null);
}
複製代碼

首先看它的構造函數,從參數來看,有滑動速度、當前位置、滑動範圍和 spring 信息(質量、剛度、摩擦等),而後,在初始化的時候,又分三種狀況,underscroll、overscroll 和其餘。三種狀況對應的三種不一樣的滑動方式。

首先,總體來講,在 BouncingScrollSimulation 中滑動也是分階段的,由於它對應的 BouncingScrollPhysics 是一個容許 overscroll 的 ScrollPhysics,因此這就致使它的減速過程也變得複雜,須要考慮是不是 overscroll 狀態下的減速,以及減速過程當中是否會產生 overscroll。因此在 BouncingScrollSimulation 中,_springSimulation 負責由 overscroll 狀態下回滾到邊界的過程,_frictionSimulation 纔是負責減速過程。

下面就直接看先減速後回彈的狀況,首先,是根據當前速度建立 _frictionSimulation 並判斷它是否會 overscroll,若是會,就再計算到達邊界的時間,而後再約過邊界的瞬間,啓用 _springSimulation。而 _springTime 就是區分的中界線,

double x(double time) => _simulation(time).x(time - _timeOffset);

Simulation _simulation(double time) {
  final Simulation simulation;
  if (time > _springTime) {
    _timeOffset = _springTime.isFinite ? _springTime : 0.0;
    simulation = _springSimulation;
  } else {
    _timeOffset = 0.0;
    simulation = _frictionSimulation;
  }
  return simulation..tolerance = tolerance;
}
複製代碼

對於任何一個函數,它都是須要經過 _simulation 先拿到當前使用的 Simulation 再計算。從這個角度上看,BouncingScrollSimulation 只是一個代理,它的全部實現都是經過 _springSimulation 和 _frictionSimulation 完成的。

直到動畫結束,一個完整的滑動過程也基本結束了。

擴展

上面介紹的還只是滑動體系的一部分,除此以外,還有更多不一樣的 ScrollPhysics,不一樣的 ScrollPosition,固然基本的邏輯都是如此。

不一樣的 ScrollPhysics 表明着不一樣的滑動方式,好比 NeverScrollableScrollPhysics,表示不可滑動,好比 PageScrollPhysics,將滑動固定在頁與頁之間。又好比 _NestedScrollPosition,專門用於控制 NestedScrollView 中,多層 view 同時滑動的邏輯,好比 _PagePosition,爲 PageView 細化了一些滑動規則等等,這些都是基於當前所描述的 ScrollPosition 規則,可是在一些函數上有了新的實現,從而勝任不一樣的目標,這些都值得去看。

相關文章
相關標籤/搜索