美文网首页记录自学flutter点点滴滴
Flutter 学习之旅(十七) 可滚动控件ScrollV

Flutter 学习之旅(十七) 可滚动控件ScrollV

作者: Tsm_2020 | 来源:发表于2020-08-19 15:46 被阅读0次

    本来想跳过真个这个直接去说一下listview还有GridView的属性,但是想着既然是学习就不能只为了怎么用他而学习,应该是通过看源码学习他的思想和原理.
    先来看一下ScrollView的构造方法

      const ScrollView({
        Key key,
        ///Scrollable属性,用来设置滑动的主轴方向
        this.scrollDirection = Axis.vertical,
        ///是否按照阅读方向相反的方向滑动
        this.reverse = false,
        ///Scrollable属性,控制器用来监听滚动和设置滚动距离
        this.controller,
        ///  指是否使用widget树中默认的PrimaryScrollController;当滑动方向为垂直方向  
        ///(scrollDirection值为Axis.vertical)并且没有指定controller时,primary默认为true
        ///看到 属性介绍primary 如果为真的时候即使他没有足够的高度来实际滚动他也会滚动,
        ///但是要求 controller 为 null ,但是我哦试验了一下没有作用
        bool primary,
        ///Scrollable 属性,完成拖拽后的动画响应
        ScrollPhysics physics,
        ///如果滚动视图不收缩换行,则滚动视图将展开到scrollDirection中允许的最大大小。
        ///如果滚动视图在scrollDirection中具有无限约束,则shrinkWrap必须为true
        /// 貌似这个属性可以解决listview嵌套的问题,但是这样更为消耗性能
        this.shrinkWrap = false,
        this.center,
        ///当scrollOffset = 0,第一个child在viewport的位置(0 <= anchor <= 1.0),
        ///0.0在开始,1.0在尾部,0.5在中间,只有
        this.anchor = 0.0,
        ///缓存区域大小
        this.cacheExtent,
        ///Scrollable 属性 语义子集数
        this.semanticChildCount,
        ///Scrollable  属性,开始响应拖拽的时机
        this.dragStartBehavior = DragStartBehavior.start,
        this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
      })
    

    结合ListView 学习 ScrollView,

    查看ScrollView你会发现他是一个抽象类

    abstract class ScrollView extends StatelessWidget {
      @protected
      List<Widget> buildSlivers(BuildContext context);
    }
    

    通过原来的学习我们应该知道所有的控件在build方法里面构建的
    我们先来看一下ScrollView 的build方法,

    
      @override
      Widget build(BuildContext context) {
        final List<Widget> slivers = buildSlivers(context);
        final AxisDirection axisDirection = getDirection(context);
    
        final ScrollController scrollController =
            primary ? PrimaryScrollController.of(context) : controller;
        final Scrollable scrollable = Scrollable(
          dragStartBehavior: dragStartBehavior,
          axisDirection: axisDirection,
          controller: scrollController,
          physics: physics,
          semanticChildCount: semanticChildCount,
          viewportBuilder: (BuildContext context, ViewportOffset offset) {
            return buildViewport(context, offset, axisDirection, slivers);
          },
        );
        final Widget scrollableResult = primary && scrollController != null
            ? PrimaryScrollController.none(child: scrollable)
            : scrollable;
    
        if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) {
          return NotificationListener<ScrollUpdateNotification>(
            child: scrollableResult,
            onNotification: (ScrollUpdateNotification notification) {
              final FocusScopeNode focusScope = FocusScope.of(context);
              if (notification.dragDetails != null && focusScope.hasFocus) {
                focusScope.unfocus();
              }
              return false;
            },
          );
        } else {
          return scrollableResult;
        }
      }
    

    buildSlivers 这个方法被ScrollView 的子类BoxScrollView的实现了,但是BoxScrollView 也是一个抽象类

    abstract class BoxScrollView extends ScrollView {
      @protected
      Widget buildChildLayout(BuildContext context);
    }
      @override
      List<Widget> buildSlivers(BuildContext context) {
        Widget sliver = buildChildLayout(context);
        EdgeInsetsGeometry effectivePadding = padding;
        if (padding == null) {
          final MediaQueryData mediaQuery = MediaQuery.of(context, nullOk: true);
          if (mediaQuery != null) {
            // Automatically pad sliver with padding from MediaQuery.
            final EdgeInsets mediaQueryHorizontalPadding =
                mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
            final EdgeInsets mediaQueryVerticalPadding =
                mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
            // Consume the main axis padding with SliverPadding.
            effectivePadding = scrollDirection == Axis.vertical
                ? mediaQueryVerticalPadding
                : mediaQueryHorizontalPadding;
            // Leave behind the cross axis padding.
            sliver = MediaQuery(
              data: mediaQuery.copyWith(
                padding: scrollDirection == Axis.vertical
                    ? mediaQueryHorizontalPadding
                    : mediaQueryVerticalPadding,
              ),
              child: sliver,
            );
          }
        }
    
        if (effectivePadding != null)
          sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
        return <Widget>[ sliver ];
      }
    

    BoxScrollView 将widget 封装成了Sliver

    通过ListView 我发现这个方法被实现了

      @override
      Widget buildChildLayout(BuildContext context) {
        if (itemExtent != null) {
          return SliverFixedExtentList(
            delegate: childrenDelegate,
            itemExtent: itemExtent,
          );
        }
        return SliverList(delegate: childrenDelegate);
      }
    

    好好分析一下这个过程,

    1.ScrollView 想要构建就必须需要子类实现buildSlivers提供Slivers,

    2.他的子类BoxScrollView 实现了buildSlivers 方法,在创建buildSlivers过程需要将Widget封装到Slivers里面,

    3.Slivers构建需要子类提供Widget,再去查看ListView 的buildChildLayout 方法时发现这个widget是由childrenDelegate构造的SliverList这个提供的,childrenDelegate是在ListView 构造方法哪里初始化的,

    class SliverList extends SliverMultiBoxAdaptorWidget {
      /// Creates a sliver that places box children in a linear array.
      const SliverList({
        Key key,
        @required SliverChildDelegate delegate,
      }) : super(key: key, delegate: delegate);
    
      @override
      RenderSliverList createRenderObject(BuildContext context) {
        final SliverMultiBoxAdaptorElement element = context as SliverMultiBoxAdaptorElement;
        return RenderSliverList(childManager: element);
      }
    }
    

    4.但是他们绘制方法在RenderSliverList 这个里面,在这个RenderSliverList 类中有一个performLayout这个方法,负责在顶部创建更多子级如果有必要的话,再沿着列表进行更新和布局

    每一个孩子,如果有必要的话在最后加上更多,直到我们有足够的覆盖整个视口的子对象。

    但是这里好多变量不知道他是什么意思,所以就理解不到他到底为什么这么做,只就看懂了一部分逻辑,从网上找的也是很久以前的源码解析了,Flutter 的代码更新的还是比较快的,不过能看出来这里包含回收和缓存,

    @override
      void performLayout() {
        final SliverConstraints constraints = this.constraints;
        childManager.didStartLayout();
        childManager.setDidUnderflow(false);
    
        final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin;
        assert(scrollOffset >= 0.0);
        final double remainingExtent = constraints.remainingCacheExtent;
        assert(remainingExtent >= 0.0);
        final double targetEndScrollOffset = scrollOffset + remainingExtent;
        final BoxConstraints childConstraints = constraints.asBoxConstraints();
        int leadingGarbage = 0;
        int trailingGarbage = 0;
        bool reachedEnd = false;
    
        // This algorithm in principle is straight-forward: find the first child
        // that overlaps the given scrollOffset, creating more children at the top
        // of the list if necessary, then walk down the list updating and laying out
        // each child and adding more at the end if necessary until we have enough
        // children to cover the entire viewport.
        //
        // It is complicated by one minor issue, which is that any time you update
        // or create a child, it's possible that the some of the children that
        // haven't yet been laid out will be removed, leaving the list in an
        // inconsistent state, and requiring that missing nodes be recreated.
        //
        // To keep this mess tractable, this algorithm starts from what is currently
        // the first child, if any, and then walks up and/or down from there, so
        // that the nodes that might get removed are always at the edges of what has
        // already been laid out.
    
        // Make sure we have at least one child to start from.
        if (firstChild == null) {
          if (!addInitialChild()) {
            // There are no children.
            geometry = SliverGeometry.zero;
            childManager.didFinishLayout();
            return;
          }
        }
    
        // We have at least one child.
    
        // These variables track the range of children that we have laid out. Within
        // this range, the children have consecutive indices. Outside this range,
        // it's possible for a child to get removed without notice.
        RenderBox leadingChildWithLayout, trailingChildWithLayout;
    
        RenderBox earliestUsefulChild = firstChild;
    
        // A firstChild with null layout offset is likely a result of children
        // reordering.
        //
        // We rely on firstChild to have accurate layout offset. In the case of null
        // layout offset, we have to find the first child that has valid layout
        // offset.
        if (childScrollOffset(firstChild) == null) {
          int leadingChildrenWithoutLayoutOffset = 0;
          while (childScrollOffset(earliestUsefulChild) == null) {
            earliestUsefulChild = childAfter(firstChild);
            leadingChildrenWithoutLayoutOffset += 1;
          }
          // We should be able to destroy children with null layout offset safely,
          // because they are likely outside of viewport
          collectGarbage(leadingChildrenWithoutLayoutOffset, 0);
          assert(firstChild != null);
        }
    
        // Find the last child that is at or before the scrollOffset.
        earliestUsefulChild = firstChild;
        for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild);
            earliestScrollOffset > scrollOffset;
            earliestScrollOffset = childScrollOffset(earliestUsefulChild)) {
          // We have to add children before the earliestUsefulChild.
          earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
    
          if (earliestUsefulChild == null) {
            final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
            childParentData.layoutOffset = 0.0;
    
            if (scrollOffset == 0.0) {
              // insertAndLayoutLeadingChild only lays out the children before
              // firstChild. In this case, nothing has been laid out. We have
              // to lay out firstChild manually.
              firstChild.layout(childConstraints, parentUsesSize: true);
              earliestUsefulChild = firstChild;
              leadingChildWithLayout = earliestUsefulChild;
              trailingChildWithLayout ??= earliestUsefulChild;
              break;
            } else {
              // We ran out of children before reaching the scroll offset.
              // We must inform our parent that this sliver cannot fulfill
              // its contract and that we need a scroll offset correction.
              geometry = SliverGeometry(
                scrollOffsetCorrection: -scrollOffset,
              );
              return;
            }
          }
    
          final double firstChildScrollOffset = earliestScrollOffset - paintExtentOf(firstChild);
          // firstChildScrollOffset may contain double precision error
          if (firstChildScrollOffset < -precisionErrorTolerance) {
            // The first child doesn't fit within the viewport (underflow) and
            // there may be additional children above it. Find the real first child
            // and then correct the scroll position so that there's room for all and
            // so that the trailing edge of the original firstChild appears where it
            // was before the scroll offset correction.
            // TODO(hansmuller): do this work incrementally, instead of all at once,
            // i.e. find a way to avoid visiting ALL of the children whose offset
            // is < 0 before returning for the scroll correction.
            double correction = 0.0;
            while (earliestUsefulChild != null) {
              assert(firstChild == earliestUsefulChild);
              correction += paintExtentOf(firstChild);
              earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
            }
            earliestUsefulChild = firstChild;
            if ((correction - earliestScrollOffset).abs() > precisionErrorTolerance) {
              geometry = SliverGeometry(
                scrollOffsetCorrection: correction - earliestScrollOffset,
              );
              final SliverMultiBoxAdaptorParentData childParentData = firstChild.parentData as SliverMultiBoxAdaptorParentData;
              childParentData.layoutOffset = 0.0;
              return;
            }
          }
    
          final SliverMultiBoxAdaptorParentData childParentData = earliestUsefulChild.parentData as SliverMultiBoxAdaptorParentData;
          childParentData.layoutOffset = firstChildScrollOffset;
          assert(earliestUsefulChild == firstChild);
          leadingChildWithLayout = earliestUsefulChild;
          trailingChildWithLayout ??= earliestUsefulChild;
        }
    
        // At this point, earliestUsefulChild is the first child, and is a child
        // whose scrollOffset is at or before the scrollOffset, and
        // leadingChildWithLayout and trailingChildWithLayout are either null or
        // cover a range of render boxes that we have laid out with the first being
        // the same as earliestUsefulChild and the last being either at or after the
        // scroll offset.
    
        assert(earliestUsefulChild == firstChild);
        assert(childScrollOffset(earliestUsefulChild) <= scrollOffset);
    
        // Make sure we've laid out at least one child.
        if (leadingChildWithLayout == null) {
          earliestUsefulChild.layout(childConstraints, parentUsesSize: true);
          leadingChildWithLayout = earliestUsefulChild;
          trailingChildWithLayout = earliestUsefulChild;
        }
    
        // Here, earliestUsefulChild is still the first child, it's got a
        // scrollOffset that is at or before our actual scrollOffset, and it has
        // been laid out, and is in fact our leadingChildWithLayout. It's possible
        // that some children beyond that one have also been laid out.
    
        bool inLayoutRange = true;
        RenderBox child = earliestUsefulChild;
        int index = indexOf(child);
        double endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
        bool advance() { // returns true if we advanced, false if we have no more children
          // This function is used in two different places below, to avoid code duplication.
          assert(child != null);
          if (child == trailingChildWithLayout)
            inLayoutRange = false;
          child = childAfter(child);
          if (child == null)
            inLayoutRange = false;
          index += 1;
          if (!inLayoutRange) {
            if (child == null || indexOf(child) != index) {
              // We are missing a child. Insert it (and lay it out) if possible.
              child = insertAndLayoutChild(childConstraints,
                after: trailingChildWithLayout,
                parentUsesSize: true,
              );
              if (child == null) {
                // We have run out of children.
                return false;
              }
            } else {
              // Lay out the child.
              child.layout(childConstraints, parentUsesSize: true);
            }
            trailingChildWithLayout = child;
          }
          assert(child != null);
          final SliverMultiBoxAdaptorParentData childParentData = child.parentData as SliverMultiBoxAdaptorParentData;
          childParentData.layoutOffset = endScrollOffset;
          assert(childParentData.index == index);
          endScrollOffset = childScrollOffset(child) + paintExtentOf(child);
          return true;
        }
    
        // Find the first child that ends after the scroll offset.
        while (endScrollOffset < scrollOffset) {
          leadingGarbage += 1;
          if (!advance()) {
            assert(leadingGarbage == childCount);
            assert(child == null);
            // we want to make sure we keep the last child around so we know the end scroll offset
            collectGarbage(leadingGarbage - 1, 0);
            assert(firstChild == lastChild);
            final double extent = childScrollOffset(lastChild) + paintExtentOf(lastChild);
            geometry = SliverGeometry(
              scrollExtent: extent,
              paintExtent: 0.0,
              maxPaintExtent: extent,
            );
            return;
          }
        }
    
        // Now find the first child that ends after our end.
        while (endScrollOffset < targetEndScrollOffset) {
          if (!advance()) {
            reachedEnd = true;
            break;
          }
        }
    
        // Finally count up all the remaining children and label them as garbage.
        if (child != null) {
          child = childAfter(child);
          while (child != null) {
            trailingGarbage += 1;
            child = childAfter(child);
          }
        }
    
        // At this point everything should be good to go, we just have to clean up
        // the garbage and report the geometry.
    
        collectGarbage(leadingGarbage, trailingGarbage);
    
        assert(debugAssertChildListIsNonEmptyAndContiguous());
        double estimatedMaxScrollOffset;
        if (reachedEnd) {
          estimatedMaxScrollOffset = endScrollOffset;
        } else {
          estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
            constraints,
            firstIndex: indexOf(firstChild),
            lastIndex: indexOf(lastChild),
            leadingScrollOffset: childScrollOffset(firstChild),
            trailingScrollOffset: endScrollOffset,
          );
          assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild));
        }
        final double paintExtent = calculatePaintOffset(
          constraints,
          from: childScrollOffset(firstChild),
          to: endScrollOffset,
        );
        final double cacheExtent = calculateCacheOffset(
          constraints,
          from: childScrollOffset(firstChild),
          to: endScrollOffset,
        );
        final double targetEndScrollOffsetForPaint = constraints.scrollOffset + constraints.remainingPaintExtent;
        geometry = SliverGeometry(
          scrollExtent: estimatedMaxScrollOffset,
          paintExtent: paintExtent,
          cacheExtent: cacheExtent,
          maxPaintExtent: estimatedMaxScrollOffset,
          // Conservative to avoid flickering away the clip during scroll.
          hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint || constraints.scrollOffset > 0.0,
        );
    
        // We may have started the layout while scrolled to the end, which would not
        // expose a new child.
        if (estimatedMaxScrollOffset == endScrollOffset)
          childManager.setDidUnderflow(true);
        childManager.didFinishLayout();
      }
    

    虽然你看着感觉这章里面我所有的东西都是在贴源码,但是为了读懂这个过程我用了整整2个小时才弄明白这个里面的逻辑

    我学习flutter的整个过程都记录在里面了
    https://www.jianshu.com/c/36554cb4c804

    最后附上demo 地址

    https://github.com/tsm19911014/tsm_flutter

    相关文章

      网友评论

        本文标题:Flutter 学习之旅(十七) 可滚动控件ScrollV

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