Unverified Commit 3a1a2533 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[framework] avoid compositing with visibility (#111844)

parent 1b583a83
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
...@@ -168,7 +168,7 @@ class Visibility extends StatelessWidget { ...@@ -168,7 +168,7 @@ class Visibility extends StatelessWidget {
/// [child] subtree is not trivial then it is significantly cheaper to not /// [child] subtree is not trivial then it is significantly cheaper to not
/// even keep the state (see [maintainState]). /// even keep the state (see [maintainState]).
/// ///
/// If this property is true, [Opacity] is used instead of [Offstage]. /// If this property is false, [Offstage] is used.
/// ///
/// If this property is false, then [maintainSemantics] and /// If this property is false, then [maintainSemantics] and
/// [maintainInteractivity] must also be false. /// [maintainInteractivity] must also be false.
...@@ -222,9 +222,9 @@ class Visibility extends StatelessWidget { ...@@ -222,9 +222,9 @@ class Visibility extends StatelessWidget {
child: child, child: child,
); );
} }
return Opacity( return _Visibility(
opacity: visible ? 1.0 : 0.0, visible: visible,
alwaysIncludeSemantics: maintainSemantics, maintainSemantics: maintainSemantics,
child: result, child: result,
); );
} }
...@@ -410,8 +410,7 @@ class SliverVisibility extends StatelessWidget { ...@@ -410,8 +410,7 @@ class SliverVisibility extends StatelessWidget {
/// [sliver] subtree is not trivial then it is significantly cheaper to not /// [sliver] subtree is not trivial then it is significantly cheaper to not
/// even keep the state (see [maintainState]). /// even keep the state (see [maintainState]).
/// ///
/// If this property is true, [SliverOpacity] is used instead of /// If this property is false, [SliverOffstage] is used.
/// [SliverOffstage].
/// ///
/// If this property is false, then [maintainSemantics] and /// If this property is false, then [maintainSemantics] and
/// [maintainInteractivity] must also be false. /// [maintainInteractivity] must also be false.
...@@ -460,9 +459,9 @@ class SliverVisibility extends StatelessWidget { ...@@ -460,9 +459,9 @@ class SliverVisibility extends StatelessWidget {
ignoringSemantics: !visible && !maintainSemantics, ignoringSemantics: !visible && !maintainSemantics,
); );
} }
return SliverOpacity( return _SliverVisibility(
opacity: visible ? 1.0 : 0.0, visible: visible,
alwaysIncludeSemantics: maintainSemantics, maintainSemantics: maintainSemantics,
sliver: result, sliver: result,
); );
} }
...@@ -495,3 +494,132 @@ class SliverVisibility extends StatelessWidget { ...@@ -495,3 +494,132 @@ class SliverVisibility extends StatelessWidget {
properties.add(FlagProperty('maintainInteractivity', value: maintainInteractivity, ifFalse: 'maintainInteractivity')); properties.add(FlagProperty('maintainInteractivity', value: maintainInteractivity, ifFalse: 'maintainInteractivity'));
} }
} }
// A widget that conditionally hides its child, but without the forced compositing of `Opacity`.
//
// A fully opaque `Opacity` widget is required to leave its opacity layer in the layer tree. This
// forces all parent render objects to also composite, which can break a simple scene into many
// different layers. This can be significantly more expensive, so the issue is avoided by a
// specialized render object that does not ever force compositing.
class _Visibility extends SingleChildRenderObjectWidget {
const _Visibility({ required this.visible, required this.maintainSemantics, super.child });
final bool visible;
final bool maintainSemantics;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderVisibility(visible, maintainSemantics);
}
@override
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
(renderObject as _RenderVisibility)
..visible = visible
..maintainSemantics = maintainSemantics;
}
}
class _RenderVisibility extends RenderProxyBox {
_RenderVisibility(this._visible, this._maintainSemantics);
bool get visible => _visible;
bool _visible;
set visible(bool value) {
if (value == visible) {
return;
}
_visible = value;
markNeedsPaint();
}
bool get maintainSemantics => _maintainSemantics;
bool _maintainSemantics;
set maintainSemantics(bool value) {
if (value == maintainSemantics) {
return;
}
_maintainSemantics = value;
markNeedsSemanticsUpdate();
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (maintainSemantics || visible) {
super.visitChildrenForSemantics(visitor);
}
}
@override
void paint(PaintingContext context, Offset offset) {
if (!visible) {
return;
}
super.paint(context, offset);
}
}
// A widget that conditionally hides its child, but without the forced compositing of `SliverOpacity`.
//
// A fully opaque `SliverOpacity` widget is required to leave its opacity layer in the layer tree.
// This forces all parent render objects to also composite, which can break a simple scene into many
// different layers. This can be significantly more expensive, so the issue is avoided by a
// specialized render object that does not ever force compositing.
class _SliverVisibility extends SingleChildRenderObjectWidget {
const _SliverVisibility({ required this.visible, required this.maintainSemantics, Widget? sliver })
: super(child: sliver);
final bool visible;
final bool maintainSemantics;
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderSliverVisibility(visible, maintainSemantics);
}
@override
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
(renderObject as _RenderSliverVisibility)
..visible = visible
..maintainSemantics = maintainSemantics;
}
}
class _RenderSliverVisibility extends RenderProxySliver {
_RenderSliverVisibility(this._visible, this._maintainSemantics);
bool get visible => _visible;
bool _visible;
set visible(bool value) {
if (value == visible) {
return;
}
_visible = value;
markNeedsPaint();
}
bool get maintainSemantics => _maintainSemantics;
bool _maintainSemantics;
set maintainSemantics(bool value) {
if (value == maintainSemantics) {
return;
}
_maintainSemantics = value;
markNeedsSemanticsUpdate();
}
@override
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
if (maintainSemantics || visible) {
super.visitChildrenForSemantics(visitor);
}
}
@override
void paint(PaintingContext context, Offset offset) {
if (!visible) {
return;
}
super.paint(context, offset);
}
}
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -424,4 +425,50 @@ void main() { ...@@ -424,4 +425,50 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('Visibility does not force compositing when visible and maintain*', (WidgetTester tester) async {
await tester.pumpWidget(
const Visibility(
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: Text('hello', textDirection: TextDirection.ltr),
),
);
// Root transform from the tester and then the picture created by the text.
expect(tester.layers, hasLength(2));
expect(tester.layers, isNot(contains(isA<OpacityLayer>())));
expect(tester.layers.last, isA<PictureLayer>());
});
testWidgets('SliverVisibility does not force compositing when visible and maintain*', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: CustomScrollView(
slivers: <Widget>[
SliverVisibility(
maintainSize: true,
maintainAnimation: true,
maintainState: true,
sliver: SliverList(
delegate: SliverChildListDelegate.fixed(
addRepaintBoundaries: false,
<Widget>[
Text('hello'),
],
),
))
]
),
),
);
// This requires a lot more layers due to including sliver lists which do manage additional
// offset layers. Just trust me this is one fewer layers than before...
expect(tester.layers, hasLength(6));
expect(tester.layers, isNot(contains(isA<OpacityLayer>())));
expect(tester.layers.last, isA<PictureLayer>());
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment