【Flutter】熊孩子拆组件系列之拆ListView(二)—— ScrollController 和 Scrollable

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

前言

上一篇中对ListView做了个基本分析,整理了下一个ListView是由哪些东西组成了;

那么现在准备好电钻锤子螺丝刀,该开始拆解了;那么首先呢,先从外面的那个壳子拆解开始;

目录

juejin.cn/post/701653…

首先要说明的

首先要提及的一点,上文中说明的PrimaryScroollController仅仅是默认提供出来的ScrollerController,如果提供了自定义的ScrollerController,那么自定义Controller将取代PrimaryScroollController;

在build方法中,是这么规定的:

final ScrollController? scrollController =
    primary ? PrimaryScrollController.of(context) : controller;
复制代码

在这里先以PrimaryScrollController为展开起点;毕竟大差不差,其本质也就一个InheritedWidget,最后作用还是提供一个ScrollController用来共享,和普通ScrollController 相比,也就多包了一层,从分析流程上没啥太大影响;

ScrollController和其涉及的部分的概念分析

首先定位到 PrimaryScrollController 最先出现的位置,ScrollView的build方法:

@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,
    scrollBehavior: scrollBehavior,
    semanticChildCount: semanticChildCount,
    restorationId: restorationId,
    viewportBuilder: (BuildContext context, ViewportOffset offset) {
      return buildViewport(context, offset, axisDirection, slivers);
    },
  );
  final Widget scrollableResult = primary && scrollController != null
      ? PrimaryScrollController.none(child: scrollable)
      : scrollable;

  if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) {
    return NotificationListener<ScrollUpdateNotification>(
      child: scrollableResult,
      onNotification: (ScrollUpdateNotification notification) {
        final FocusScopeNode focusScope = FocusScope.of(context);
        if (notification.dragDetails != null && focusScope.hasFocus) {
          focusScope.unfocus();
        }
        return false;
      },
    );
  } else {
    return scrollableResult;
  }
}
复制代码

从build方法的角度分析:需要理解的东西有这么三个:

  1. buildSlivers
  2. ScrollController
  3. Scrollable

buildSlivers

在源码中是这么规定的:

/// Build the list of widgets to place inside the viewport.
///
/// Subclasses should override this method to build the slivers for the inside
/// of the viewport.
@protected
List<Widget> buildSlivers(BuildContext context);
复制代码

注释中也说明了,就是提供一个包裹在ViewPort的widget列表;说白了,提供ListView的子View列表;

至于这个新出的ViewPort?正好在目录清单中,那就先放着不管~~ 到时候再去了解这部分的内容;

ScrollController

来到这篇的主题了,还是按照惯例,看下注释中是如何定义ScrollController的:

 Controls a scrollable widget.

 Scroll controllers are typically stored as member variables in [State]
 objects and are reused in each [State.build]. A single scroll controller can
 be used to control multiple scrollable widgets, but some operations, such
 as reading the scroll [offset], require the controller to be used with a
 single scrollable widget.

 A scroll controller creates a [ScrollPosition] to manage the state specific
 to an individual [Scrollable] widget. To use a custom [ScrollPosition],
 subclass [ScrollController] and override [createScrollPosition].

 A [ScrollController] is a [Listenable]. It notifies its listeners whenever
 any of the attached [ScrollPosition]s notify _their_ listeners (i.e.
 whenever any of them scroll). It does not notify its listeners when the list
 of attached [ScrollPosition]s changes.

 Typically used with [ListView], [GridView], [CustomScrollView].

复制代码

一句话翻译:

ScrollController 本质是一个 Listenable ,会在列表滚动的时候通知其监听器;其对滑动组件的管理,是通过构造出 ScrollPosition ,并将其提供给 Scrollable来实现的;其存活于Widget 的整个生命周期,在整个build过程中不断复用;

嗯,就这么一段话又抛出了几个名词:

  1. ScrollPosition
  2. Listenable

还是先解决名词部分,至少不至于明白说的什么东西:

ScrollPosition

 Determines which portion of the content is visible in a scroll view.

 The [pixels] value determines the scroll offset that the scroll view uses to
 select which part of its content to display. As the user scrolls the
 viewport, this value changes, which changes the content that is displayed.

 The [ScrollPosition] applies [physics] to scrolling, and stores the
 [minScrollExtent] and [maxScrollExtent].

 Scrolling is controlled by the current [activity], which is set by
 [beginActivity]. [ScrollPosition] itself does not start any activities.
 Instead, concrete subclasses, such as [ScrollPositionWithSingleContext],
 typically start activities in response to user input or instructions from a
 [ScrollController].

 This object is a [Listenable] that notifies its listeners when [pixels]
 changes.

 ## Subclassing ScrollPosition

 Over time, a [Scrollable] might have many different [ScrollPosition]
 objects. For example, if [Scrollable.physics] changes type, [Scrollable]
 creates a new [ScrollPosition] with the new physics. To transfer state from
 the old instance to the new instance, subclasses implement [absorb]. See
 [absorb] for more details.

 Subclasses also need to call [didUpdateScrollDirection] whenever
 [userScrollDirection] changes values.

复制代码

一句话翻译:

ScrollPosition 本质同样还是一个 Listenable ,会在列表滚动的时候通知其监听器;作用是管理一个滚动widget的可视子Widget;在其中存储有随着滚动而不断变化的pixels,以及minScrollExtentmaxScrollExtent;物理效果是一个名为activity的东西来处理的,应该就是一种模拟计算器,会计算惯性之类的,这个模拟器会由ScrollPosition 的具体子类,例如;

这么看,好像跟之前猜测的ViewPort 功能差不多……但是考虑到ScrollPosition本质上为一个Listenable,因此猜测其仅仅当作数据存储、计算,最后通知其他人干活的地方,而ViewPort 会根据这里的数据,来变化展示效果;这块后续验证下

Listenable

 An object that maintains a list of listeners.

 The listeners are typically used to notify clients that the object has been
 updated.

 There are two variants of this interface:

  * [ValueListenable], an interface that augments the [Listenable] interface
    with the concept of a _current value_.

  * [Animation], an interface that augments the [ValueListenable] interface
    to add the concept of direction (forward or reverse).

 Many classes in the Flutter API use or implement these interfaces. The
 following subclasses are especially relevant:

  * [ChangeNotifier], which can be subclassed or mixed in to create objects
    that implement the [Listenable] interface.

  * [ValueNotifier], which implements the [ValueListenable] interface with
    a mutable value that triggers the notifications when modified.

 The terms "notify clients", "send notifications", "trigger notifications",
 and "fire notifications" are used interchangeably.
复制代码

按照注释所述,这块也就仅仅是个普通的提供通知功能的监听器而已;

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.
复制代码

按注释说明,Scrollable跟Container一样,是一种组合Widget而已;

从build方法来看,其作用就是将_ScrollableScope(一个InheritedWidget,用来共享上面的ScrollPosition以及Scrollable的State本身)、RawGestureDetector(手势监听器)、IgnorePointer(手势过滤)组合起来,根据不同情况,启用或者禁用对应功能,提供数据计算等功能;

ScrollController 是如何工作的

那么基本概念简单分析了下,下面就看下ScrollController是如何工作的;

先从build方法看起,分析ListView是如何驱动起来的

ScrollableState 的 build 方法中,抛开其他边边角角的东西,是这么组合的:

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

复制代码

1、 首先,通过一个_ScrollableScope,共享了position和 ScrollableState 本身;目前来看,这部分并未参与到ScrollController的工作流程中,先无视掉……

题外话:不过检查其调用位置,会发现有个有点意思的东西 : ScrollPosition 中有个名为recommendDeferredLoading的方法;看注释,这个方法作用是延迟加载,如果返回ture,就视为滚动速度过快,影响UI的昂贵操作应该被推迟…………此方法是flutter的滑动控件,在滑动中延迟加载图片的实现途径;或许可以在这个方法上再加个队列之类的东西,综合判断返回值,来实现一个延迟加载组件?

2、 下面就是一个 Listener + RawGestureDetector 的组合;

在这里的Listener的作用,估计是为了保障结果的准确,看注释 PointerSignal 是一个非连续的事件消息,也就是那种不像hold、drag那种,没跟down绑定开始的事件;在这里加个 Listener ,估计是为了让这种事件的结果,也能够处理体现;

3、 再之后就是手势监听器 RawGestureDetector

RawGestureDetector 这里通过设置 gestures 来当做驱动事件计算的扳机;具体可以在 setCanDrag 方法中看到其被初始化,并绑定各种诸如 _handleDragDown 的方法;

而触发setCanDrag方法的地方,就是第一次ViewPort的layout的时候,会触发 ScrollPositionapplyViewportDimension,并将一个标志Dimension是否发生改变的布尔值改为true,之后再走到触发 applyContentDimensions 的时候,就会调用 applyNewDimensions 方法,而在ScrollPostion的具体实现类 —— ScrollPositionWithSingleContext 中,重写了applyNewDimensions方法,并调用了 setCanDrag,实现初始化 ;自此,手势监听与对应的触发计算功能正式上线;

而具体的手势处理方法,包含那些 _handleDragDown之类的方法,中间包含的_hold、_drag的方法,其本质也无非是一个计算器而已,连接了ScrollPosition和其对应实现的模拟器Activity;并根据手势数据,调用activity计算数据并设置给Position;

至于ViewPort中是怎么绑定了ScrollPosition,这些Position为什么有更新,就能更新界面,ViewPortScrollPosition的关系是什么?那就等ViewPort部分解析的时候再分析吧;

4、 最后是一个IgnorePointer 作用嘛,也挺简单的,根据手势操作拦截往下分发的事件,举个例子,如果在拖拽listView,那么拖拽过程中的事件,就没必要往下接着传递;

从build方法来看,ListView的驱动过程也不复杂,手势监听器将事件传递给Position,并交给它来计算,更新,进而触发绘制之类的;

那么ScrollController好像没什么存在意义啊?我好像只需要一个ScrollPosition就行了??

根据上面ScrollController的注释定义,其参与过程,也是通过构造提供Position来实现的,甚至build方法中也是position在刷存在感;那么这么一说,好像ScrollController没啥存在意义啊……甚至其本身包含注释部分也就200多行,好像也没啥作用?????

我是不是可以删掉这玩意,只提供一个position出去就行???

在ListView中,ScrollController确实好像没啥存在感,在ScrollableState中,其被调用的地方也就创建一个position,然后attach和detach;

但是看下源码,ScrollController 的 position 是以List形式存储的;也就是说存在可能绑定多个position的情况;看下继承关系,确实ScrollController中存在像_NestedScrollController这种专门还写了处理positions的东西;

这么看来ScrollController的根本作用,应该就是为了统一管理position,提供统一绑定,绑定和解绑通知等功能;要看核心逻辑,还是关注ScrollPosition;

或许这篇的标题,应该改成ScrollPosition 和 Scrollable ?

文章太长不看的懒人总结篇

整体核心还是要看 ScrollerPostion ;

Scrollable 构造了手势监听器,并将手势监听结果通知给ScrollPosition,供其计算处理通知展示;

ScrollController 的作用也就是为ScrollPosition服务,提供统一的注册、绑定通知等功能;核心功能还是要看ScrollPosition;(仅以ListView来说,如果要看像NestedScrollerView这种,那就不是这么简单的东西了

而ScrollPosition 的作用就是通过其内置的一堆模拟器、计算器等,计算出手势操作的结果,并通知ViewPort等部分重新测量重新绘制等;

猜你喜欢

转载自juejin.im/post/7018025872451960869