美文网首页All in FlutterFlutterFlutter
Flutter笔记-深入分析滑动控件

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

作者: 叶落清秋 | 来源:发表于2019-01-28 17:55 被阅读100次

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

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

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


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

    相关文章

      网友评论

        本文标题:Flutter笔记-深入分析滑动控件

        本文链接:https://www.haomeiwen.com/subject/zoqajqtx.html