這篇將著重描述 Flutter 中滾動機制中,怎麼對應 Drag 狀態發送 ScrollNotification

這裡不清楚 Scrollable 怎麼跟 ScrollActivity 互動,唯一知道的是 ScrollActivity 判斷要不要發送 ScrollUpdateNotification

Scrollable

Scrollable 很少會直接被使用。

/// A widget that scrolls.
///
/// [Scrollable] implements the interaction model for a scrollable widget,
/// including gesture recognition, but does not have an opinion about how the
/// viewport, which actually displays the children, is constructed.
///
/// It's rare to construct a [Scrollable] directly. Instead, consider [ListView]
/// or [GridView], which combine scrolling, viewporting, and a layout model. To
/// combine layout models (or to use a custom layout mode), consider using
/// [CustomScrollView].
///
/// The static [Scrollable.of] and [Scrollable.ensureVisible] functions are
/// often used to interact with the [Scrollable] widget inside a [ListView] or
/// a [GridView].
///
/// To further customize scrolling behavior with a [Scrollable]:
///
/// 1. You can provide a [viewportBuilder] to customize the child model. For
///    example, [SingleChildScrollView] uses a viewport that displays a single
///    box child whereas [CustomScrollView] uses a [Viewport] or a
///    [ShrinkWrappingViewport], both of which display a list of slivers.
///
/// 2. You can provide a custom [ScrollController] that creates a custom
///    [ScrollPosition] subclass. For example, [PageView] uses a
///    [PageController], which creates a page-oriented scroll position subclass
///    that keeps the same page visible when the [Scrollable] resizes.

整理類圖如下:

classDiagram
class Scrollable
class ScrollableState
class _ScrollableScope
class ScrollNotification

ScrollableState.setCanDrag 被呼叫時,便會把 drag event handler 綁定到 GestureRecognizer。如下面程式碼所示。

	..onDown = _handleDragDown
    ..onStart = _handleDragStart
    ..onUpdate = _handleDragUpdate
    ..onEnd = _handleDragEnd
    ..onCancel = _handleDragCancel

這時 _handleDragStart 會負責處理 Drag 的狀態跟 Scroll 狀態的對應關係,因為不是每一個 drag 都是 scrolling,所以要判對是不是在滾動當前的 widget, 如果判斷是滾動,則讓 ScrollDragController 去發送對應的 Scroll 事件,讓 ScrollView 建立的 NotificationListener 可以收到,並做對應的事件處理。

  @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(

_handleDragStart()

使用 ScrollPosition.darg

  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);
  }

ScrollPosition.drag 是抽象方法 ,這裡以 ScrollPositionWithSingleContext.darg 實作解釋:

  @override
  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;
  }

所以 drag 的生命週期是由 ScrollDragController 控制。

ScrollDragController

classDiagram

class Drag {
  <<interface>>
}

class ScrollActivity {
  <<abstract>>
}

class ScrollNotification {
  <<abstract>>
}

class ScrollActivityDelegate {
  <<interface>>
}

class ScrollDragController
class DragScrollActivity

ScrollActivity <|-- DragScrollActivity

Drag <|-- ScrollDragController
ScrollActivity --> ScrollActivityDelegate
DragScrollActivity --> ScrollDragController
DragScrollActivity --> ScrollNotification

classDiagram

class Drag {
	<<abstract>>
}

class GestureRecognizer {
  <<abstract>>
}

class PanView {
  <<abstract>>
}

class CustomPanView {
  <<abstract>>
}

class Panable
class PanableState

class PanController
class PanMoveController

class PanActivity {
  <<abstract>>
}

class DragPanActivity

class PanPosition {
  <<abstract>>
}

class Viewport {
  <<abstract>>
}

class ViewportOffset {
  <<abstract>>
}

class PanNotification {
  <<abstract>>
}

ViewportOffset <|-- PanPosition
Drag <|-- PanDragController
PanActivity <|-- DragPanActivity

PanView <|-- CustomPanView
CustomPanView --> Panable : 建立
Panable --> PanableState : 建立
PanableState --> NotificationListener : 建立
NotificationListener --> PanNotification : 處理
PanController --> PanPosition : 建立
PanableState --> PanPosition : 使用
PanableState --> PanMoveController : 建立
PanableState --> PanController : 使用
PanableState --> Viewport : 建立
Viewport --> ViewportOffset
PanMoveController --> PanMoveActivity : 建立
PanActivity --> PanNotification : 發送
PanableState --> GestureRecognizer : 綁定 handler
GestureRecognizer --> Drag : 建立

ScrollActivityDelegate

實作 ScrollActivityDelegate 介面的類別有好幾個,但主要實作的是 ScrollPositionWithSingleContext,定義在 ~src/widgets/scroll_position_with_single_context.dart~ScrollPositionWithSingleContext 會用 ScrollContext 管理當前 scrolling 的狀態,其實就是 "Scrollable" widget 的 State object。 記住,很多時候,我們需要追蹤多個 scrolling 的狀態,而這個 delegator 只處理 single context 的情境。

類圖整理如下:


classDiagram

class ScrollActivityDelegate {
	<<interface>>
}

class ScrollPositionWithSingleContext {
  <<interface>>
}


class ScrollContext {
  <<interface>>
}

ScrollActivityDelegate <|-- ScrollPositionWithSingleContext
ScrollPositionWithSingleContext --> ScrollContext
ScrollPositionWithSingleContext --> IdleScrollActivity
ScrollPositionWithSingleContext --> DragScrollActivity
ScrollPositionWithSingleContext --> HoldScrollActivity

ScrollContext <|-- ScrollableState

ScrollActivity

ScrollActivity 是一個抽象類別,分別對應不同的滾動活動,子類別如下:

  • BallisticScrollActivity
  • HoldScrollActivity
  • DragScrollActivity
  • DrivenScrollActivity
  • IdleScrollActivity

其中 IdleScrollActivity 是初始狀態,在 drag 開始時,才進入 DragScrollActivity