preface

Today, I read The Flutter Interact, and a little sister translated the whole time (simultaneous translation, so strong), so I finished this article while listening.

Following on from the previous chapter Flutter lifetime enemies of silver (ExtendedList), in this chapter we will write a waterfall flow layout to verify the correctness of our source analysis of the Sliver list in the previous chapter.

Welcome to Flutter CandiesQQ group: 181398081.

  • Flutter Silver’s Lifetime Enemy (ScrollView)
  • Flutter silver lifetime enemies (ExtendedList)
  • Flutter Sliver You want waterfall flow little sister
  • Flutter silver locks your beauty

I know you only care about the little sister, but LET me show you the effects first.

The principle of

I did waterfall flow layout myself when I was working on UWP. It seems to be an obsession, after the pit Flutter, to also achieve a waterfall flow layout. Here’s a brief description of what a waterfall flow is and how it works.

The waterfall flow layout is characterized by equal width and unequal height. To minimize the gap in the last row, start with the second row by placing an item under the lowest item in the first row, and so on, as shown in the figure below. 4 is under 0, 5 is under 3, 6 is under 1, 7 is under 2, 8 is under 4…

The core code

Now that we know the principle, let’s implement the principle into code.

  • Because we need to know the Items closest to the top of the viewport, and the Items closest to the bottom of the viewport. So that you know what item to put the new item under when you scroll backwards, or what item to put the new item on top of when you scroll forward

I designed CrossAxisItems to hold leadingItems and trailingItems.

  • To add a new item backwards, the code looks like this

1. Add leadingItems until they equal crossAxisCount

2. Find the shortest item and set its layoutoffset

3. Save the indexes in this column

  void insert({
    @required RenderBox child,
    @required ChildTrailingLayoutOffset childTrailingLayoutOffset,
    @required PaintExtentOf paintExtentOf,
  }) {
    final WaterfallFlowParentData data = child.parentData;
    final LastChildLayoutType lastChildLayoutType =
        delegate.getLastChildLayoutType(data.index);
    
    ///Handle the last specialization layout
    switch (lastChildLayoutType) {
      case LastChildLayoutType.fullCrossAxisExtend:
      case LastChildLayoutType.foot:
        // Draw offset on the horizontal axis
        data.crossAxisOffset = 0.0;
        / / transverse index
        data.crossAxisIndex = 0;
        // The size of the child
        final size = paintExtentOf(child);
        
        if (lastChildLayoutType == LastChildLayoutType.fullCrossAxisExtend ||
            maxChildTrailingLayoutOffset + size >
                constraints.remainingPaintExtent) {
          data.layoutOffset = maxChildTrailingLayoutOffset;
        } else {
          // If all children are not drawn as large as the viewport
          data.layoutOffset = constraints.remainingPaintExtent - size;
        }
        data.trailingLayoutOffset = childTrailingLayoutOffset(child);
        return;
      case LastChildLayoutType.none:
        break;
    }

    if(! leadingItems.contains(data)) {// Fill leadingItems
      if(leadingItems.length ! = crossAxisCount) { data.crossAxisIndex ?? = leadingItems.length; data.crossAxisOffset = delegate.getCrossAxisOffset(constraints, data.crossAxisIndex);if (data.index < crossAxisCount) {
          data.layoutOffset = 0.0;
          data.indexs.clear();
        }

        trailingItems.add(data);
        leadingItems.add(data);
      } else {
        if(data.crossAxisIndex ! =null) {
          var item = trailingItems.firstWhere(
              (x) =>
                  x.index > data.index &&
                  x.crossAxisIndex == data.crossAxisIndex,
              orElse: () => null);

          ///out of viewport
          if(item ! =null) {
            data.trailingLayoutOffset = childTrailingLayoutOffset(child);
            return; }}// Find the shortest one
        var min = trailingItems.reduce((curr, next) =>
            ((curr.trailingLayoutOffset < next.trailingLayoutOffset) ||
                    (curr.trailingLayoutOffset == next.trailingLayoutOffset &&
                        curr.crossAxisIndex < next.crossAxisIndex)
                ? curr
                : next));

        data.layoutOffset = min.trailingLayoutOffset + delegate.mainAxisSpacing;
        data.crossAxisIndex = min.crossAxisIndex;
        data.crossAxisOffset =
            delegate.getCrossAxisOffset(constraints, data.crossAxisIndex);

        trailingItems.forEach((f) => f.indexs.remove(min.index));
        min.indexs.add(min.index);
        data.indexs = min.indexs;
        trailingItems.remove(min);
        trailingItems.add(data);
      }
    }

    data.trailingLayoutOffset = childTrailingLayoutOffset(child);
  }
Copy the code
  • To add a new item forward, the code looks like this

1. Find which column the new item belongs to through indexs

2. Add to the old item

  void insertLeading({
    @required RenderBox child,
    @required PaintExtentOf paintExtentOf,
  }) {
    final WaterfallFlowParentData data = child.parentData;
    if(! leadingItems.contains(data)) {var pre = leadingItems.firstWhere((x) => x.indexs.contains(data.index),
          orElse: () => null);

      if (pre == null || pre.index < data.index) return; data.trailingLayoutOffset = pre.layoutOffset - delegate.mainAxisSpacing; data.crossAxisIndex = pre.crossAxisIndex; data.crossAxisOffset = delegate.getCrossAxisOffset(constraints, data.crossAxisIndex); leadingItems.remove(pre); leadingItems.add(data); trailingItems.remove(pre); trailingItems.add(data); data.indexs = pre.indexs; data.layoutOffset = data.trailingLayoutOffset - paintExtentOf(child); }}Copy the code
  • Calculate which items are closest to the top of the viewport. Make sure leadingItems are inside the viewport

Similar to the previous Listview source analysis, except here we want to make sure that the largest LeadingLayoutOffset is smaller than the scrollOffset, so that the leadingItems are in the viewport

    if (crossAxisItems.maxLeadingLayoutOffset > scrollOffset) {
      RenderBox child = firstChild;
      //move to max index of leading
      final int maxLeadingIndex = crossAxisItems.maxLeadingIndex;
      while(child ! =null && maxLeadingIndex > indexOf(child)) {
        child = childAfter(child);
      }
      //fill leadings from max index of leading to min index of leading
      while(child ! =null && crossAxisItems.minLeadingIndex < indexOf(child)) {
        crossAxisItems.insertLeading(
            child: child, paintExtentOf: paintExtentOf);
        child = childBefore(child);
      }
      //collectGarbage(maxLeadingIndex - index, 0);

      while (crossAxisItems.maxLeadingLayoutOffset > scrollOffset) {
        // We have to add children before the earliestUsefulChild.
        earliestUsefulChild =
            insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);

        if (earliestUsefulChild == null) {
          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; crossAxisItems.reset(); crossAxisItems.insert( child: earliestUsefulChild, childTrailingLayoutOffset: childTrailingLayoutOffset, paintExtentOf: paintExtentOf, );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;
          }
        }

        crossAxisItems.insertLeading(
            child: earliestUsefulChild, paintExtentOf: paintExtentOf);

        final WaterfallFlowParentData data = earliestUsefulChild.parentData;

        // firstChildScrollOffset may contain double precision error
        if (data.layoutOffset < -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.
          // 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);
            crossAxisItems.insertLeading(
                child: earliestUsefulChild, paintExtentOf: paintExtentOf);
          }
          geometry = SliverGeometry(
            scrollOffsetCorrection: correction - data.layoutOffset,
          );
          return;
        }

        assert(earliestUsefulChild == firstChild);
        leadingChildWithLayout = earliestUsefulChild;
        trailingChildWithLayout ??= earliestUsefulChild;
      }
    }
Copy the code
  • The calculation reaches the bottom of the viewport, and you should make sure that the shortest of the trailingItems is beyond the bottom of the viewport
    // Now find the first child that ends after our end.
    if(child ! =null) {
      while (crossAxisItems.minChildTrailingLayoutOffset <
              targetEndScrollOffset ||
              //make sure leading children are painted. 
          crossAxisItems.leadingItems.length < _gridDelegate.crossAxisCount
          || crossAxisItems.leadingItems.length  > childCount
          ) {
        if(! advance()) { reachedEnd =true;
          break; }}}Copy the code

waterfall_flowuse

  • Add a library reference in pubspec.yaml

dependencies:
  waterfall_flow: any

Copy the code
  • Import libraries

  import 'package:waterfall_flow/waterfall_flow.dart';
  
Copy the code

How to define

You can set SliverWaterfallFlowDelegate parameters to define the waterfall flow

parameter describe The default
crossAxisCount The number of elements of equal length on the horizontal axis mandatory
mainAxisSpacing The distance between the main elements 0.0
crossAxisSpacing The distance between elements on the horizontal axis 0.0
collectGarbage Callback when the element is reclaimed
lastChildLayoutTypeBuilder The layout style for the last element (see later for details)
viewportBuilder Callback when element indexes change in the visual area
closeToTrailing Can the layout be trailing(see trailing for details) false
            WaterfallFlow.builder(
              / / cacheExtent: 0.0.
              padding: EdgeInsets.all(5.0),
              gridDelegate: SliverWaterfallFlowDelegate(
                  crossAxisCount: 2,
                  crossAxisSpacing: 5.0,
                  mainAxisSpacing: 5.0./// follow max child trailing layout offset and layout with full cross axis extend
                  /// last child as loadmore item/no more item in [GridView] and [WaterfallFlow]
                  /// with full cross axis extend
                  // LastChildLayoutType.fullCrossAxisExtend,

                  /// as foot at trailing and layout with full cross axis extend
                  /// show no more item at trailing when children are not full of viewport
                  /// if children is full of viewport, it's the same as fullCrossAxisExtend
                  // LastChildLayoutType.foot,
                  lastChildLayoutTypeBuilder: (index) => index == _list.length
                      ? LastChildLayoutType.foot
                      : LastChildLayoutType.none,
                  ),

Copy the code

Complete little sister Demo

conclusion

There is no more analysis of the source code, if you read the last article, you should be more clear. This article is a demonstration of the waterfall flow principle implemented on Flutter. There is no impossible effect, only unexpected effect. This is my experience brought by Flutter.

Finally put some content of Flutter Interact, I wrote it while listening, please remind me to change if there is any mistake.

  • Flutter version 1.12
  • Material Design and font library
  • Brought Desktop/Web(you can try them now, not toys)
  • Various development tools (simultaneously debugging 7 kinds of equipment, UI interface)
  • Gskinner cool interactive + open source
  • Supernova’s famous design-to-code tool
  • Tremble Adobe XD program monkeys, UI will take your place.
  • Flutter_ Vignettes Trade blows
  • Rive. App shows a cute game of stir-fry chicken, and it’s done with the Flutter Web

Welcome to joinFlutter CandiesAnd produce cute little Flutter candies (QQ group: 181398081)

Finally put the Flutter on the bucket. It smells good.