Skip to content

The RepaintBoundary widget shatters the tree #101810

@flar

Description

@flar

In concept, the RepaintBoundary sounds like a great idea because it isolates the tree into sections that may be changing a lot in isolation from the rest of the tree that might not be changing - and vice versa.

In reality, due to the way it is implemented, it shatters the tree into many sections depending on the depth of the RepaintBoundary. This shattering could be reduced by a better handling of the parents of a composited child.

Here is an example to demonstrate the point...

Currently, when you add a RepaintBoundary to an app, it splits the tree down the middle completely shattering it. You go from 1 "display list" (as the framework documentation calls it, but it really could be a combination of Layers and DisplayLists in implementation) to a new tree that one might expect to have 2 or 3 sections (a before section, a repaint boundary'd section, and an after section). In the case of my app, it goes from 1 to 21 sections. This is due to the way that the Flutter tree handles the properties it refers to as "compositing" properties. If anything in the tree is composited then all parent widgets that might otherwise just draw to the Canvas instead all become "Layers" that cause their parents to also need to "composite".

Example code!
import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'RepaintBoundary Tree Impact Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'RepaintBoundary Tree Impact Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _useRepaintBoundary = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: _NestedWidget(
          level: 10,
          rotateDegrees: 9,
          useRepaintBoundary: _useRepaintBoundary,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => _useRepaintBoundary = !_useRepaintBoundary),
        tooltip: '${_useRepaintBoundary ? 'Remove' : 'Add'} RepaintBoundary',
        child: Icon(_useRepaintBoundary ? Icons.remove : Icons.add),
      ),
    );
  }
}

class _NestedWidget extends StatelessWidget {
  const _NestedWidget({
    required this.level,
    required this.rotateDegrees,
    required this.useRepaintBoundary,
  });

  final int level;
  final double rotateDegrees;
  final bool useRepaintBoundary;

  @override
  Widget build(BuildContext context) {
    if (level == 0) {
      Widget child = const SizedBox(width: 200, height: 300);
      if (useRepaintBoundary) {
        child = RepaintBoundary(child: child);
      }
      return child;
    } else {
      return Transform(
        transform: Matrix4.rotationZ(rotateDegrees * pi / 180),
        alignment: Alignment.center,
        child: CustomPaint(
          painter: _MyPainter(color: Colors.blue, isHorizontal: true),
          foregroundPainter: _MyPainter(color: Colors.green, isHorizontal: false),
          child: _NestedWidget(
            level: level - 1,
            rotateDegrees: rotateDegrees,
            useRepaintBoundary: useRepaintBoundary,
          ),
        ),
      );
    }
  }
}

class _MyPainter extends CustomPainter {
  _MyPainter({required this.color, required this.isHorizontal});

  final Color color;
  final bool isHorizontal;

  @override
  void paint(Canvas canvas, Size size) {
    Paint p = Paint()
        ..color = color;
    if (isHorizontal) {
      canvas.drawRect(Rect.fromLTWH(0, size.height / 2 - 5, size.width, 10), p);
    } else {
      canvas.drawRect(Rect.fromLTWH(size.width / 2 - 5, 0, 10, size.height), p);
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

An observatory view of it before I click the button to add the RepaintBoundary:

Screen Shot 2022-04-13 at 12 08 08 AM

And another observatory view after I click to add the RepaintBoundary. Note that the RepaintBoundary is only added to the leaf widget!

Screen Shot 2022-04-13 at 12 09 16 AM

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listc: performanceRelates to speed or footprint issues (see "perf:" labels)found in release: 2.10Found to occur in 2.10found in release: 2.13Found to occur in 2.13frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work onteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions