Flutter笔记-深入分析滑动控件

ps: 文中flutter源码版本 1.0.0

通过分析各个滑动控件,如:ListViewPageViewSingleChildScrollView等,内部都有一个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()方法,同时先查看Statebuild(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的构造函数传递过来的

因此,我们来看看SingleChildScrollViewviewportBuilder是什么

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):

《Flutter笔记-深入分析滑动控件》 image.png

很明显,当子类过大的时候只有当到底部才满足该条件,因此效果上是除非滑动底部或子类足够小,否则裁剪画布,去除超出部分

所以,滑动的过程也就是不断改变绘制位置的过程

源码:https://github.com/leaf-fade/flutterDemo/blob/master/lib/scroll/widget/scrollable.dart

    原文作者:叶落清秋
    原文地址: https://www.jianshu.com/p/5de174e82175
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞