ps: 文中flutter源碼版本 1.0.0
通過分析各個滑動控件,如:ListView
、PageView
、SingleChildScrollView
等,內部都有一個Scrollable
控件
也就是說滑動其實就是靠的Scrollable
控件,這里就通過源碼對其進行分析
class Scrollable extends StatefulWidget {
/// Creates a widget that scrolls.
///
/// The [axisDirection] and [viewportBuilder] arguments must not be null.
const Scrollable({
Key key,
this.axisDirection = AxisDirection.down,
this.controller,
this.physics,
@required this.viewportBuilder,
this.excludeFromSemantics = false,
this.semanticChildCount,
}) : assert(axisDirection != null),
assert(viewportBuilder != null),
assert(excludeFromSemantics != null),
super (key: key);
//滑動方向,上下左右四方向
final AxisDirection axisDirection;
//滑動控制
final ScrollController controller;
/* 滑動效果
* AlwaysScrollableScrollPhysics() 總是可以滑動,默認值
* NeverScrollableScrollPhysics禁止滾動
* BouncingScrollPhysics 內容超過一屏 上拉有回彈效果
* ClampingScrollPhysics 包裹內容 不會有回彈
*/
final ScrollPhysics physics;
//關鍵,
final ViewportBuilder viewportBuilder;
//語義控件,輔助工具相關
final bool excludeFromSemantics;
final int semanticChildCount;
Axis get axis => axisDirectionToAxis(axisDirection);
@override
ScrollableState createState() => ScrollableState();
...
}
StatefulWidget
控件直奔createState()
方法,同時先查看State
的build(BuildContext context)
方法
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
implements ScrollContext {
...
@override
Widget build(BuildContext context) {
assert(position != null);
//RawGestureDetector,手勢監聽控件
Widget result = RawGestureDetector(
key: _gestureDetectorKey,
gestures: _gestureRecognizers,
behavior: HitTestBehavior.opaque,
excludeFromSemantics: widget.excludeFromSemantics,
//Semantics 語義控件,輔助控件相關,不考慮
child: Semantics(
explicitChildNodes: !widget.excludeFromSemantics,
//IgnorePointer,語義控件相關,不考慮
child: IgnorePointer(
key: _ignorePointerKey,
ignoring: _shouldIgnorePointer,
ignoringSemantics: false,
//InheritedWidget控件,主要是為了共享position數據,即包含了physics的ScrollPosition對象
child: _ScrollableScope(
scrollable: this,
position: position,
child: widget.viewportBuilder(context, position),
),
),
),
);
//含Semantics直接跳過,不相關
if (!widget.excludeFromSemantics) {
result = _ScrollSemantics(
key: _scrollSemanticsKey,
child: result,
position: position,
allowImplicitScrolling: widget?.physics?.allowImplicitScrolling ?? _physics.allowImplicitScrolling,
semanticChildCount: widget.semanticChildCount,
);
}
return _configuration.buildViewportChrome(context, result, widget.axisDirection);
}
...
}
為什么android滑動超過界限的效果和ios不同處理
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
return child;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
//水波紋滑動越界效果
return GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
color: _kDefaultGlowColor,
);
}
return null;
}
在_ScrollableScope
私有控件中,child: widget.viewportBuilder(context, position)
是什么
typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset position);
viewportBuilder
是一個方法,返回一個widget
,而值是通過Scrollable
的構造函數傳遞過來的
因此,我們來看看SingleChildScrollView
的viewportBuilder
是什么
class SingleChildScrollView extends StatelessWidget {
...
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
Widget contents = child;
if (padding != null)
contents = Padding(padding: padding, child: contents);
final ScrollController scrollController = primary
? PrimaryScrollController.of(context)
: controller;
final Scrollable scrollable = Scrollable(
axisDirection: axisDirection,
controller: scrollController,
physics: physics,
//就是這里,這個傳遞的offset即上面的position,是一個ScrollPosition對象
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return _SingleChildViewport(
axisDirection: axisDirection,
offset: offset,
child: contents,
);
},
);
return primary && scrollController != null
? PrimaryScrollController.none(child: scrollable)
: scrollable;
}
}
_SingleChildViewport
是一個SingleChildRenderObjectWidget
,突然有些熟悉了,就是自繪控件那,直接找createRenderObject
方法
class _SingleChildViewport extends SingleChildRenderObjectWidget {
...
@override
_RenderSingleChildViewport createRenderObject(BuildContext context) {
return _RenderSingleChildViewport(
axisDirection: axisDirection,
offset: offset,
);
}
...
}
class _RenderSingleChildViewport extends RenderBox
with RenderObjectWithChildMixin<RenderBox>
implements RenderAbstractViewport {...}
源碼考慮的情況比較多,這里我們做個簡化,只考慮垂直方向,并且是向下的(基于源碼重寫了一個類,刪減了源碼的部分內容)
class _RenderChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox>{
_RenderChildViewport({
@required ViewportOffset offset,
}):_offset = offset;
ViewportOffset _offset;
ViewportOffset get offset => _offset;
set offset(ViewportOffset value) {
assert(value != null);
if (value == _offset)
return;
if (attached)
_offset.removeListener(_hasScrolled);
_offset = value;
if (attached)
//offset變化則重繪并更新
_offset.addListener(_hasScrolled);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
_offset.addListener(_hasScrolled);
}
@override
void detach() {
_offset.removeListener(_hasScrolled);
super.detach();
}
void _hasScrolled() {
//重繪和更新
markNeedsPaint();
markNeedsSemanticsUpdate();
}
@override
void performLayout() {
...
}
@override
void paint(PaintingContext context, Offset offset) {
...
}
}
}
_offset
增加了監聽,一旦發生了變化,就會調用_hasScrolled()
,從而重新繪制,調用paint(PaintingContext context, Offset offset)
從2個方面來看:
1.擺放
class _RenderChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox>{
...
@override
void performLayout() {
//如果有子控件,子控件內部的擺放就交給子控件自己
if (child == null) {
size = constraints.smallest;
} else {
child.layout(constraints.widthConstraints(), parentUsesSize: true);
//約束布局,傳遞的size約束在屏幕內
size = constraints.constrain(child.size);
}
//size.height 父控件的高度
offset.applyViewportDimension(size.height);
//child.size.height 子控件的高度
offset.applyContentDimensions(0.0, child.size.height - size.height);
}
}
layout過程對size進行了計算,同時設置了offset約束范圍
abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
...
//賦值,賦予父控件的高度
@override
bool applyViewportDimension(double viewportDimension) {
if (_viewportDimension != viewportDimension) {
_viewportDimension = viewportDimension;
_didChangeViewportDimensionOrReceiveCorrection = true;
}
return true;
}
//最大及最小滑動距離
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
if (!nearEqual(_minScrollExtent, minScrollExtent, Tolerance.defaultTolerance.distance) ||
!nearEqual(_maxScrollExtent, maxScrollExtent, Tolerance.defaultTolerance.distance) ||
_didChangeViewportDimensionOrReceiveCorrection) {
_minScrollExtent = minScrollExtent;
_maxScrollExtent = maxScrollExtent;
_haveDimensions = true;
applyNewDimensions();
_didChangeViewportDimensionOrReceiveCorrection = false;
}
return true;
}
}
2.繪制
class _RenderChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox>{
...
Offset get _paintOffset => _paintOffsetForPosition(offset.pixels);
Offset _paintOffsetForPosition(double position) {
return Offset(0.0, -position);
}
bool _shouldClipAtPaintOffset(Offset paintOffset) {
assert(child != null);
return paintOffset < Offset.zero || !(Offset.zero & size).contains((paintOffset & child.size).bottomRight);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
final Offset paintOffset = _paintOffset;
void paintContents(PaintingContext context, Offset offset) {
//從偏移點處開始繪制子控件,因為是上向滑動,所以這里的paintOffset是負值
context.paintChild(child, offset + paintOffset);
}
//是否需要裁剪
if (_shouldClipAtPaintOffset(paintOffset)) {
//矩形裁剪
context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintContents);
} else {
paintContents(context, offset);
}
}
}
}
重點分析一下_shouldClipAtPaintOffset(paintOffset)
abstract class OffsetBase {
...
bool operator <(OffsetBase other) => _dx < other._dx && _dy < other._dy;
Rect operator &(Size other) => new Rect.fromLTWH(dx, dy, other.width, other.height);
}
paintOffset < Offset.zero || !(Offset.zero & size).contains((paintOffset & child.size).bottomRight)
paintOffset < Offset.zero
,不滿足,因為dx不變,就看第二個,圖解一下(Offset.zero & size).contains((paintOffset & child.size).bottomRight
):
很明顯,當子類過大的時候只有當到底部才滿足該條件,因此效果上是除非滑動底部或子類足夠小,否則裁剪畫布,去除超出部分
所以,滑動的過程也就是不斷改變繪制位置的過程
源碼:https://github.com/leaf-fade/flutterDemo/blob/master/lib/scroll/widget/scrollable.dart