-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Description
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:
And another observatory view after I click to add the RepaintBoundary. Note that the RepaintBoundary is only added to the leaf widget!

