Flutter笔记-深入分析滑动控件
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;
//滑动相关的一些数据
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.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