滾動操做在 UI 交互設計中是不可或缺的,由於咱們的顯示設備大小畢竟有限,沒有辦法一次性展現全部的信息,因此只有藉助於滾動操做,才能夠向用戶展現更多的內容。markdown
在 Flutter 中,一樣有許多滾動相關的 widget,像 SingleChildScrollView
、ListView
和 GridView
等,都是可滾動 widget。它們的使用也很廣泛。因此掌握它們的用法是頗有必要的。咱們比較熟悉的滾動相關類應該非 ScrollController
莫屬了。那麼就從第一位最被人熟知的劍客提及。ide
顧名思義,ScrollController 的做用是控制一個可滾動的控件。一個 ScrollController 能夠被用於多個可滾動控件,可是當咱們經過 ScrollController#offset
來獲取滾動量時,這時就要求此 ScrollController 只能被一個可滾動控件綁定,不然就會拋出斷言異常。動畫
咱們首先看一下 ScrollController 的結構。ui
從上圖能夠看到,它的方法很少,主要功能就是對外暴露位置接口,其次就是能夠經過 animateTo()
和 jumpTo()
方法設置可滾動控件滾動到指定位置。那麼從它的這兩個主要使命出發,咱們看看它都是怎麼實現的。this
offset
spa
double get offset => position.pixels;
翻譯
offset 是一個 get 方法,真正返回的是 position 的 pixels 成員的值,而這個 position,從上面的結構圖裏,能夠看到它是一個 ScrollPosition 的類型,至於這個 ScrollPosition 類的做用和實現是什麼樣的,先按下不表,繼續往下看。debug
animateTo()
和 jumpTo()
設計
Future<void> animateTo(
double offset, {
@required Duration duration,
@required Curve curve,
}) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
final List<Future<void>> animations = List<Future<void>>(_positions.length);
for (int i = 0; i < _positions.length; i += 1)
animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve);
return Future.wait<void>(animations).then<void>((List<void> _) => null);
}
void jumpTo(double value) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
for (final ScrollPosition position in List<ScrollPosition>.from(_positions))
position.jumpTo(value);
}
複製代碼
這兩個方法的做用都是滾動到指定的位置,區別就是 animateTo()
方法是伴隨着動畫滾動到目的位置,而 jumpTo
方法則不伴隨動畫,直接跳轉到目的位置,它們都藉助 ScrollPosition 對象來完成。3d
能夠看到,上面幾個方法的實現中,都是使用了 position 的對應方法。那麼與位置相關的數據的管理看來都是由 ScrollPosition 類來完成的了。在深刻了解 ScrollPosition以前,咱們先看看 ScrollPosition 對象是如何建立的。
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition,
) {
return ScrollPositionWithSingleContext(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
複製代碼
上面的方法建立了一個 ScrollPositionWithSingleContext 對象,ScrollPositionWithSingleContext 類是 ScrollPosition 的子類,不少的操做都是在它內部實現的。好了,下面咱們就來揭開 ScrollPosition 的神祕面紗。
官方文檔對它的描述是:決定滾動視圖的哪一部份內容是可見的。從上面 ScrollController 也能夠知道,滾動視圖的滾動位置等屬性以及使滾動視圖滾動到指定位置等操做都是由 ScrollPosition 完成。那就先分別看看 ScrollController 裏的屬性獲取和操做方法具體是怎麼實現的。
pixels
ScrollController 的 offset 屬性獲取的就是 ScrollPosition 的 pixels 值,它指向私有變量 _pixels
。
_pixels 的值寫入的地方有下面幾個:
先跳過這一部分,再看看兩外兩個操做方法。
jumpTo()
@override
void jumpTo(double value) {
goIdle();
if (pixels != value) {
final double oldPixels = pixels;
// 設置當前的滾動像素數
// (另外,根據上面的圖,這個方法會更新 ScrollPosition 的 pixels,
// 同即 ScroController 的 offset)
forcePixels(value);
// 處理位置跳轉前的回調
didStartScroll();
// 處理位置變化回調
didUpdateScrollPositionBy(pixels - oldPixels);
// 處理滾動完成回調
didEndScroll();
}
goBallistic(0.0);
}
複製代碼
goIdle()
方法將當前的滾動狀態置爲閒置。
@override
void goIdle() {
beginActivity(IdleScrollActivity(this));
}
複製代碼
至於狀態的管理和設置方法,咱們在介紹第三個劍客的時候再接着說。
而 goBallistic()
方法則是處理諸如滾動到最大程度後的回彈等狀態。
@override
void goBallistic(double velocity) {
assert(pixels != null);
final Simulation simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
} else {
goIdle();
}
}
複製代碼
animateTo()
@override
Future<void> animateTo(
double to, {
@required Duration duration,
@required Curve curve,
}) {
// 若是目的位置和如今的位置接近到一個程度,那麼直接跳轉到目的位置,而不須要再啓動動畫過程
if (nearEqual(to, pixels, physics.tolerance.distance)) {
// Skip the animation, go straight to the position as we are already close.
jumpTo(to);
return Future<void>.value();
}
final DrivenScrollActivity activity = DrivenScrollActivity(
this,
from: pixels,
to: to,
duration: duration,
curve: curve,
vsync: context.vsync,
);
beginActivity(activity);
return activity.done;
}
複製代碼
上面能夠看到位置的變換最終都是經過 beginActivity()
方法保存和更新一系列的滾動狀態。這個 ScrollActivity 正是第三位劍客。
照例,咱們先來看看這個類的結構。
經過類的結構,咱們也能夠很清晰地看到這個類地基本功能。它有4個成員變量:delegate
、shouldIgnorePointer
、isScrolling
和 velocity
,其中 delegate
是 ScrollActivityDelegate 類型,一個虛擬類,它的子類有以下這些,除了第一個,它們又都是類 ScrollPositionWithSingleContext 的子類,而 ScrollPositionWithSingleContext 類又是 ScrollPosition 的子類,因此 delegate
就是一個 ScrollPosition 對象。shouldIgnorePointer
對象的做用根據文檔的翻譯一下就是「滾動視圖在進行此滾動活動時是否應該忽視觸摸事件」。其餘兩個就很好理解了。
ScrollActivity 是一個抽象類,因此它的真正用法不在於它自己,而在於其子類的定製化,那看看它的子類有哪些。
其中的 IdleScrollActivity 咱們在 ScrollPosition 的 goIdle()
方法中已經見過,它其實模擬的就是一個沒有滾動時的滾動活動,用來標識一個滾動視圖閒置的狀態。再好比,DrivenScrollActivity(ScrollPosition 的 animateTo()
方法建立的就是這個 ScrollActivity),模擬一種動畫驅動的滾動狀態;HoldScrollActivity 模擬的是沒有作任何工做,釋放掉後就可轉換爲 IdelScrollActivity,一般模擬滾動視圖已經被觸摸但還未被拖拽的狀態,等。
三劍客大體介紹這麼多,它們基本是 ScrollController -> ScrollPosition -> ScrollActivity 的一個建立過程,而且在建立過程當中持有對方的對象實例。ScrollController 更主要的功能是對用戶暴露接口,主要的工做由 ScrollPosition 搭配 ScrollActivity 完成。
梳理了一下這三個滾動相關的重要機制後,咱們能夠發現,flutter 實際上是把滾動過程給抽象成了不一樣的部分,這三個類只是對滾動過程的一種抽象,真正的滾動和滾動過程當中的視圖更新和繪製並不禁它們負責。