滑动原理

Catalogue   

用到的API

  • Viewport:视窗,滑动可以看见的区域
  • Scrollable:对手势的处理,实现滑动效果
  • Sliver:用于在Viewport里面布局和渲染内容
  • ScrollPosition
  • ScrollDragController
  • ScrollConfiguration
  • ScrollBehavior
  • ScrollActivity
  • ScrollPhysics:可滚动控件的物理特性
    • BouncingScrollPhysics :允许滚动超出边界,但之后内容会反弹回来。
    • ClampingScrollPhysics : 防止滚动超出边界,夹住 。
    • AlwaysScrollableScrollPhysics :始终响应用户的滚动。
    • NeverScrollableScrollPhysics :不响应用户的滚动。

在Flutter中:

  • ListView使用的是SliverFixedExtentList或SliverList
  • GridView使用的是SliverGrid
  • PageView使用的是SliverFillViewport
  • SingleChildScrollView内容是单个RenderObject

滑动过程

  1. setCanDrag正式激活
  2. Scrollable中DragGestureRecognizer监听各种事件onDown,对应ScrollableState中_handleDragDown开始,对应ScrollPosition的hold
  3. ScrollableState中_handleDragStart,对应ScrollPosition的drag,构造ScrollDragController,切换状态
  4. ScrollableState中_handleDragUpdate,对应Drag的update,手指滑动响应,
  5. ScrollableState的_handleDragUpdate,对应Drag的onEnd,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
class ScrollableState {

// 对应onDown,
void _handleDragDown(DragDownDetails details) {
assert(_drag == null);
assert(_hold == null);
_hold = position.hold(_disposeHold);//ScrollPosition的hold方法创建了HoldScrollActivity
}

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);//ScrollDragController进行update
}

void _handleDragEnd(DragEndDetails details) {
// _drag might be null if the drag activity ended and called _disposeDrag.
assert(_hold == null || _drag == null);
_drag?.end(details);
assert(_drag == null);
}
}


class ScrollPositionWithSingleContext {

@override
ScrollHoldController hold(VoidCallback holdCancelCallback) {
final double previousVelocity = activity!.velocity;
final HoldScrollActivity holdActivity = HoldScrollActivity(
delegate: this,
onHoldCanceled: holdCancelCallback,
);
beginActivity(holdActivity);
_heldPreviousVelocity = previousVelocity;
return holdActivity;
}

ScrollDragController? _currentDrag;

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


//切换状态
@override
void beginActivity(ScrollActivity? newActivity) {
_heldPreviousVelocity = 0.0;
if (newActivity == null) {
return;
}
assert(newActivity.delegate == this);
super.beginActivity(newActivity);
_currentDrag?.dispose();
_currentDrag = null;
if (!activity!.isScrolling) {
updateUserScrollDirection(ScrollDirection.idle);
}
}


/// 滑动结束时的处理,首先通过 ScrollPhysics 创建一个 Simulation,然后将其传给 BallisticScrollActivity。从 BallisticScrollActivity 的构造函数可以看到,本质上我们也可以将其看作是一个由动画驱动的滑动过程,只不过这个动画是根据一个给定的初始速度创建的
@override
void goBallistic(double velocity) {
assert(hasPixels);
final Simulation? simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
//BallisticScrollActivity 与 DrivenScrollActivity 的相似度很高,它们都是在构造函数中先根据提供的信息(simulation,duration、curve等)创建一个 AnimationController,然后监听更新和结束事件,在 _tick 中更新偏移值,在 _end 中结束自己。
beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
} else {
goIdle();
}
}

}


class ScrollDragController {


@override
void update(DragUpdateDetails details) {
assert(details.primaryDelta != null);
_lastDetails = details;
double offset = details.primaryDelta!;
//_lastDetails 是为了在之后发送通知的时候,加上这个滑动事件信息,比如 dispatchScrollUpdateNotification
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) {
offset = -offset;
}
delegate.applyUserOffset(offset);//传递给ScrollPhysics,不同的physics不同的效果
}

///是否损失动量的判断
///这个过程的目的,是为了判断是否损失动量,我们知道,一般在 ios 的滑动中,连续快速滑动的时候,速度是会积累的,所以后面会越滑越快,而 flutter 为了保持这一特性,就有了动量积累这样一个功能,目前也只在 BouncingScrollPhysics 中才有
/// Determines whether to lose the existing incoming velocity when starting
/// the drag.
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;
}
}


///当滑动手势结束时,远不意味着整个滑动的结束,为了用户体验,我们赋予滑动速度的概念,那它的滑动也就有动量,所以停止不能只是戛然而止,需要一个慢慢停下来的过程,所以就有了 BallisticScrollActivity 所代表的减速过程,而这个过程的主要控制者,实际为 ScrollPhysics 所生成的 Simulation,不同的 Simulation 相距甚远
@override
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) {
velocity = -velocity;
}
_lastDetails = details;

if (_retainMomentum) {
// Build momentum only if dragging in the same direction.
final bool isFlingingInSameDirection = velocity.sign == carriedVelocity!.sign;
// Build momentum only if the velocity of the last drag was not
// substantially lower than the carried momentum.
final bool isVelocityNotSubstantiallyLessThanCarriedMomentum =
velocity.abs() > carriedVelocity!.abs() * momentumRetainVelocityThresholdFactor;
if(isFlingingInSameDirection && isVelocityNotSubstantiallyLessThanCarriedMomentum) {
velocity += carriedVelocity!;
}
}
//此时就是计算一下当前的滑动速度,以便后面进入 BallisticScrollActivity 阶段,也就是 goBallistic 调用
delegate.goBallistic(velocity);
}

}


参考