Flutter - more efficient pan and zoom for CustomPaint

2,205

Here's where I ended up.

I size my custom painter to be as large as it needs, and then I position it inside a Transform widget (that is top-left aligned with an offset of zero).

On top of this widget I overlay an invisible widget that manages touch inputs. Using a GestureDetector, it will respond to events and notify the Transform widget to update.

With the pan/zoom officially moved out of the painter, I then implemented the "shouldRepaint" function to be more strict.

This has allowed me to render very, very large grids at good-enough speeds.

Share:
2,205
Jellio
Author by

Jellio

Updated on December 14, 2022

Comments

  • Jellio
    Jellio over 1 year

    I'm rendering a collection of grids of tiles, where each tile is pulled from an image. To render this, I'm rendering everything inside my own implementation of CustomPainter (because the grids can get pretty large). To support pan and zoom functionality, I opted to perform the offsetting and scaling as part of canvas painting.

    Here is a portion of my custom painting implementation.

      @override
      void paint(Canvas canvas, Size size) {
        // With the new canvas size, we may have new constraints on min/max offset/scale.
        zoom.adjust(
          containerSize: size,
          contentSize: Size(
            (cellWidth * columnCount).toDouble(),
            (cellHeight * rowCount).toDouble(),
          ),
        );
    
        canvas.save();
        canvas.translate(zoom.offset.dx, zoom.offset.dy);
        canvas.scale(zoom.scale);
    
        // Now, draw the background image and grids.
    

    While this is functional, performance can start to breakdown after enough cells are rendered (for example, a grid of 100x100 causes some lag on each GestureDetector callback that updates the zoom values). And, because the offsetting and scaling is done in the CustomPaint, I basically can't return false for bool shouldRepaint(MyPainter old) because it needs to repaint to render its new offset and scale.

    So, my question is: What is a more performant way of approaching this problem?

    I've tried one other approach:

    var separateRenderTree = RepaintBoundary(
      child: OverflowBox(
        child: CustomPaint(
          painter: MyPainter(),
        ),
      ),
    );
    return Transform(
      transform: Matrix4.translationValues(_zoom.offset.dx, _zoom.offset.dy, 0)..scale(_zoom.scale),
      child: separateRenderTree,
    );
    

    This also works, but can also get laggy when scaling (translating is buttery smooth).

    So, again, what is the right approach to this problem?

    Thank you.

    • pskink
      pskink over 4 years
      simply draw only those images that are visible (i am assuming that 100x100 images are not visible at one time)
    • Jellio
      Jellio over 4 years
      This works most of the time, but the user can zoom out far enough to view the entire grid. I do currently have logic that only draws the images of it's within the visible viewport, but the performance gain isn't good enough.
    • pskink
      pskink over 4 years
      what's the point in showing 100x100 grid on one screen? the images would be 10 maybe 20 pixels in size...
    • Jellio
      Jellio over 4 years
      Being Flutter, I'm not limited to just mobile devices. I plan on using this on the web. 100x100 is not unrealistic.
    • pskink
      pskink over 4 years
      are you creating brand new CustomPaint on every frame? (if your CustomPainter does not eat much cpu in its ctor it shouldn't be a big deal but still ...)
    • Jellio
      Jellio over 4 years
      Yes, I am. I was following the idiomatic Flutter method of building out the tree of Widgets. I can try caching it off.
    • pskink
      pskink over 4 years
      i would not expect much but try that way: github.com/pskink/matrix_gesture_detector/blob/master/exampl‌​e/… - the whole idea is to use Listenable repaint in CustomPainter ctor - see line #49
    • Jellio
      Jellio over 4 years
      I see. I appreciate the response. After trying that out, I opted to try out the performance profiling tools available in Flutter and saw that the actual computation really just boiled down to the iteration time for hundreds of thousands of cells, even without drawing on the canvas. I think this may boil down to another constraint... maybe the problem isn't the rendering... maybe I should figure out how to store this much data before I optimize how to render it.
  • Fourj
    Fourj over 3 years
    Thanks for solution. Agreed that transform is better way to improve performance instead of repaint.
  • whidev
    whidev over 3 years
    Could you please share a snippet of the widget tree you are describing? I am having difficulties implementing your solution.