Detailed explanation of the reuse mechanism of flutter waterfall stream imitating the original

Time:2021-9-13

Nonsense opening:

IOS and Android have a reuse mechanism when implementing the list interface. The purpose is to reduce memory overhead and trade time for space. I feel that the “reuse” of listview.builder should be a process of destruction and reconstruction. Therefore, a simple reuse mechanism is implemented here with fluent. Bad code, don’t spray, make common progress

Look at the reuse effect first

Reuse status printing

On the right is the simple waterfall flow interface, which shows a total of 39 widgets. On the left is a total of 12 widgets created by console printing, so widget reuse is simply realized here.

Question 1. What is the realization idea?

Here, let’s briefly talk about the implementation idea.

  • Before rendering the interface, calculate the position coordinates of all widgets.
  • Create a visual waterfall widget for the first rendering
  • Monitor the sliding, judge the waterfall flow widget displayed in the scrolling direction of the current page, get it from the cache pool first, if not, create it directly and add it to the component for rendering. If there is in the cache pool, modify the relative layout position of the widget.

Problem 2: UI layout code analysis.

Tip: waterfallflow.dart waterfall flow main page; Waterfallflowitem.dart waterfall flow unit item

Effect display:

Waterfallflowitem.dart waterfall flow item file


class WaterfallFlowItem extends StatefulWidget{
  Frame? _frame;
  WaterfallFlowItemState? _waterfallFlowItemState;

  WaterfallFlowItem({required Frame frame}){
    _frame = frame;
  }

  Frame getFrame(){
    return _frame!;
  }

  void setFrame({required Frame frame}) {
    _frame = frame;
    _waterfallFlowItemState!.setFrame(frame: frame);
  }

  @override
  State<StatefulWidget> createState() {
    _waterfallFlowItemState = new WaterfallFlowItemState(frame: _frame!);
    return _waterfallFlowItemState!;
  }
}

class WaterfallFlowItemState extends State<WaterfallFlowItem> with AutomaticKeepAliveClientMixin {
  Frame? _frame;

  WaterfallFlowItemState({required Frame frame}){
    _frame = frame;
  }

  void setFrame({required Frame frame}) {
    setState(() {
      _frame = frame;
    });
  }
  @override
  Widget build(BuildContext context) {
    return new Positioned(
        top: _frame!.top,
        left: _frame!.left,
        child: GestureDetector(
          child: new Container(
            color: _frame!.index == 12 ? Colors.red : Color.fromARGB(255, 220, 220, 220),
            width: _frame!.width,
            height: _frame!.heigth,
            child: new Text(_frame!.index.toString()),
          ),
          onTap: (){

          },
        )
    );
  }

  @override
  // TODO: implement wantKeepAlive
  bool get wantKeepAlive => true;
}

Waterfallflow.dart main interface file

Builder implementation

@override
Widget build(BuildContext context) {
  return new Container(
    //Remove the blank space at the top of Scrollview
    child: MediaQuery.removePadding(
        context: context,
        removeTop: true,
        child: Scrollbar(
          //isAlwaysShown: true,
          //showTrackOnHover: true,
          //scrollView
          child: new SingleChildScrollView(
            controller: _scrollController,
            child: new Container(
              width: MediaQuery.of(context).size.width,
              //Maximum height
              height: _maxHeight,
              color: Colors.white,
              child: new Stack(
                //Waterfall flow cell item collection under frame layout
                children: _listWidget,
              ),
            ),
          ),
        )
  ),
  );
}

Declared properties

//Waterfall flow interval
double sep = 5;
//Waterfall flow width
double? _width;
//Maximum height
double _maxHeight = 0;
//Maximum left height
double leftHeight = 0;
//Maximum right height
double rightHeight = 0;
//Main interface height
double _mineContentHeight = 0;
//Waterfall stream item cache pool
List<WaterfallFlowItem> _bufferPoolWidget = [];
//Currently displayed waterfall flow item
List<WaterfallFlowItem> _listWidget = [];
//The current group rendering frame object is saved
List<Frame> _fList = [];
//Total frame set
List<Frame> _frameList = [];
//The data source only saves the height here
List<double> _list = [
  100,150,45,11,140,89,212,21,434,545,100,150,45,11,140,89,212,21,434,545,
  100,150,45,11,140,89,212,21,434,545,100,150,45,11,140,89,212,21,434,545];
//Sliding monitor
ScrollController _scrollController = new ScrollController();
//Slip offset
double _scrollOff = 0;

Calculate the height of the main window Scrollview

//Obtain the maximum height and calculate all waterfall flow positions
void getMaxHeight(){
  List<Frame> fList = [];
  double width = (_width! - sep * 3) / 2.0;
  double maxHeight = _maxHeight;
  for(int i = _frameList.length;i < _list.length;i++){
    double height = _list[i];
    bool isLeft = (leftHeight <= rightHeight);
    double left = isLeft ? sep : (width + sep * 2);
    maxHeight = isLeft ? leftHeight : rightHeight;
    Frame frame = Frame(leftP: left, topP: maxHeight, widthP: width, heigthP: height,indexP: i);
    if(isLeft == true) {
      leftHeight += (height + sep);
    } else {
      rightHeight += (height + sep);
    }
    fList.add(frame);
  }
  _maxHeight = max(leftHeight, rightHeight);
  _frameList.addAll(fList);
  //Refresh
  setState(() {});
}

Frame location information class

class Frame{
  double left = 0;// Left
  double top = 0;// right
  double width = 0;// width
  double heigth = 0;// height
  int index = 0;// Indexes
  Frame({required leftP
  ,required topP,
    required widthP,
    required heigthP,
    required indexP}){
    left = leftP * 1.0;
    top = topP * 1.0;
    width = widthP * 1.0;
    heigth = heigthP * 1.0;
    index = indexP;
  }
}

Generate waterfall flow widget unit item

//Generate item in reuse pool
_takeReuseFlowItem(Frame f,dynamic block){
  WaterfallFlowItem? waterfallFlowItem;
  //Reuse? Yes, modify the frame directly; No, re render.
  bool isReUse = false;
  //Yes, from the cache pool (the in the cache is already in the structure tree, and the frame layout position can be modified)
  if(_bufferPoolWidget.length > 0){
    waterfallFlowItem = _bufferPoolWidget.last;
    waterfallFlowItem.setFrame(frame: f);
    _bufferPoolWidget.removeLast();
    isReUse = true;
  }
  
  //No, create it directly (not in the cache, you need to call the setstate method for rendering)
  if(waterfallFlowItem == null) {
    waterfallFlowItem = new WaterfallFlowItem(frame: f,);
    isReUse = false;
  }
  block(waterfallFlowItem,isReUse);
}

Create all visual waterfall flow widget unit components on the first screen

//Render waterfall stream item
createWaterfallFlow(int index){
  getMaxHeight();
  //Add a little delay here to ensure that the maximum height is achieved (not too rigorous. The great God has a good method. Please give me [fist])
  Future.delayed(Duration(milliseconds: 100),(){
    _mineContentHeight = context.size!.height;
    for(var i = 0;i < _frameList.length;i++){
      Frame f = _frameList[i];
      //Judgment visualization logic
      if(f.top <= _mineContentHeight + _scrollOff) {
        _takeReuseFlowItem(f,(WaterfallFlowItem waterfallFlowItem,bool isReuse){
          _listWidget.add(waterfallFlowItem);
        });
      }
    }
    setState(() {
    });
  });
}

Reuse rendering during sliding

//Gets the position of the next item currently displayed in the slide up state
Frame? _getUpNeedShowFrame(){
  Frame? f;
  WaterfallFlowItem? lastWaterfallFlowItem = _listWidget.last;
  if(lastWaterfallFlowItem.getFrame().index + 1 < _frameList.length) {
    f = _frameList[lastWaterfallFlowItem.getFrame().index + 1];
  }
  return f;
}

//Gets the position of the last item currently displayed in the sliding status
Frame? _getDownNeedShowFrame(){
  Frame? f;
  WaterfallFlowItem? lastWaterfallFlowItem = _listWidget[0];
  if(lastWaterfallFlowItem.getFrame().index - 1 >= 0) {
    f = _frameList[lastWaterfallFlowItem.getFrame().index - 1];
  }
  return f;
}

//Waterfall streams beyond the visual range of the interface are added to the cache pool
void addFlowItemAddToBufferPool(){

  List<WaterfallFlowItem> list = [];
  for(int i = 0; i < _listWidget.length;i++){
    WaterfallFlowItem? waterfallFlowItem = _listWidget[i];
    Frame? frame = waterfallFlowItem.getFrame();
    if((frame.top + frame.heigth) <  _scrollOff || frame.top > _mineContentHeight + _scrollOff) {
      _bufferPoolWidget.add(waterfallFlowItem);
      list.add(waterfallFlowItem);
    }
  }
  if(list.length != 0) {
    for(int i= 0;i < list.length;i++){
      WaterfallFlowItem? waterfallFlowItem = list[i];
      if(_listWidget.contains(waterfallFlowItem)){
        _listWidget.remove(waterfallFlowItem);
      }
    }
  }

  //Get item from cache pool
  //Up sliding state
  Frame? upNextFrame = _getUpNeedShowFrame();
  if(upNextFrame != null) {
    //DebugPrint ('I'm reusing ${upnextframe. Index}, ${upnextframe. Top}, ${minecontentheight + _scrolloff} ');
    if(upNextFrame.top <= _mineContentHeight + _scrollOff) {
      DebugPrint ('I reset the ${upnextframe. Index} frame on the slide ');
      _takeReuseFlowItem(upNextFrame,(WaterfallFlowItem waterfallFlowItem,bool isReuse){
        _listWidget.add(waterfallFlowItem);
        if(!isReuse){
          DebugPrint ('I'm not reusing ');
          setState(() {});
        } else {
          DebugPrint ('I am reuse ');
          waterfallFlowItem.setFrame(frame: upNextFrame);
        }
      });
    }
  }

  //Sliding state
  Frame? downNextFrame = _getDownNeedShowFrame();
  if(downNextFrame != null) {
    //DebugPrint ('I'm reusing ${downnextframe. Index}, ${downnextframe. Top}, ${minecontentheight + _scrolloff} ');
    if(downNextFrame.top + downNextFrame.heigth > _scrollOff && downNextFrame.top + downNextFrame.heigth < _mineContentHeight + _scrollOff) {
      DebugPrint ('I am resetting the ${downnextframe. Index} frame ');
      _takeReuseFlowItem(downNextFrame,(WaterfallFlowItem waterfallFlowItem,bool isReuse){
        _listWidget.insert(0, waterfallFlowItem);
        if(!isReuse){
          DebugPrint ('I'm not reusing ');
          setState(() {});
        } else {
          DebugPrint ('I am reuse ');
          waterfallFlowItem.setFrame(frame: downNextFrame);
        }
      });
    }
  }
}

Scroll monitor

_scrollController.addListener(() {
  _scrollOff = _scrollController.offset;
  //Join the cache pool and reuse it
  addFlowItemAddToBufferPool();
  DebugPrint ('total: ${u listwidget. Length + _bufferpoolwidget. Length} ');
});

Basically, the waterfall stream reuse logic of FLUENT is completed. The code is poor, and some places need to be optimized, such as fast sliding protection and item content rendering. Fluent has been very extreme for interface rendering, and rewriting and reusing is a bit backward. Don’t spray. Learn from each other.

summary

This is the end of this article about the native reuse mechanism of flutter waterfall flow imitation. For more information about flutter imitation reuse mechanism, please search the previous articles of developeppaer or continue to browse the relevant articles below. I hope you will support developeppaer in the future!