Skip to content

Commit 3a1a253

Browse files
author
Jonah Williams
authored
[framework] avoid compositing with visibility (#111844)
1 parent 1b583a8 commit 3a1a253

File tree

2 files changed

+185
-10
lines changed

2 files changed

+185
-10
lines changed

packages/flutter/lib/src/widgets/visibility.dart

Lines changed: 138 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'package:flutter/foundation.dart';
5+
import 'package:flutter/rendering.dart';
66

77
import 'basic.dart';
88
import 'framework.dart';
@@ -168,7 +168,7 @@ class Visibility extends StatelessWidget {
168168
/// [child] subtree is not trivial then it is significantly cheaper to not
169169
/// even keep the state (see [maintainState]).
170170
///
171-
/// If this property is true, [Opacity] is used instead of [Offstage].
171+
/// If this property is false, [Offstage] is used.
172172
///
173173
/// If this property is false, then [maintainSemantics] and
174174
/// [maintainInteractivity] must also be false.
@@ -222,9 +222,9 @@ class Visibility extends StatelessWidget {
222222
child: child,
223223
);
224224
}
225-
return Opacity(
226-
opacity: visible ? 1.0 : 0.0,
227-
alwaysIncludeSemantics: maintainSemantics,
225+
return _Visibility(
226+
visible: visible,
227+
maintainSemantics: maintainSemantics,
228228
child: result,
229229
);
230230
}
@@ -410,8 +410,7 @@ class SliverVisibility extends StatelessWidget {
410410
/// [sliver] subtree is not trivial then it is significantly cheaper to not
411411
/// even keep the state (see [maintainState]).
412412
///
413-
/// If this property is true, [SliverOpacity] is used instead of
414-
/// [SliverOffstage].
413+
/// If this property is false, [SliverOffstage] is used.
415414
///
416415
/// If this property is false, then [maintainSemantics] and
417416
/// [maintainInteractivity] must also be false.
@@ -460,9 +459,9 @@ class SliverVisibility extends StatelessWidget {
460459
ignoringSemantics: !visible && !maintainSemantics,
461460
);
462461
}
463-
return SliverOpacity(
464-
opacity: visible ? 1.0 : 0.0,
465-
alwaysIncludeSemantics: maintainSemantics,
462+
return _SliverVisibility(
463+
visible: visible,
464+
maintainSemantics: maintainSemantics,
466465
sliver: result,
467466
);
468467
}
@@ -495,3 +494,132 @@ class SliverVisibility extends StatelessWidget {
495494
properties.add(FlagProperty('maintainInteractivity', value: maintainInteractivity, ifFalse: 'maintainInteractivity'));
496495
}
497496
}
497+
498+
// A widget that conditionally hides its child, but without the forced compositing of `Opacity`.
499+
//
500+
// A fully opaque `Opacity` widget is required to leave its opacity layer in the layer tree. This
501+
// forces all parent render objects to also composite, which can break a simple scene into many
502+
// different layers. This can be significantly more expensive, so the issue is avoided by a
503+
// specialized render object that does not ever force compositing.
504+
class _Visibility extends SingleChildRenderObjectWidget {
505+
const _Visibility({ required this.visible, required this.maintainSemantics, super.child });
506+
507+
final bool visible;
508+
final bool maintainSemantics;
509+
510+
@override
511+
RenderObject createRenderObject(BuildContext context) {
512+
return _RenderVisibility(visible, maintainSemantics);
513+
}
514+
515+
@override
516+
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
517+
(renderObject as _RenderVisibility)
518+
..visible = visible
519+
..maintainSemantics = maintainSemantics;
520+
}
521+
}
522+
523+
class _RenderVisibility extends RenderProxyBox {
524+
_RenderVisibility(this._visible, this._maintainSemantics);
525+
526+
bool get visible => _visible;
527+
bool _visible;
528+
set visible(bool value) {
529+
if (value == visible) {
530+
return;
531+
}
532+
_visible = value;
533+
markNeedsPaint();
534+
}
535+
536+
bool get maintainSemantics => _maintainSemantics;
537+
bool _maintainSemantics;
538+
set maintainSemantics(bool value) {
539+
if (value == maintainSemantics) {
540+
return;
541+
}
542+
_maintainSemantics = value;
543+
markNeedsSemanticsUpdate();
544+
}
545+
546+
@override
547+
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
548+
if (maintainSemantics || visible) {
549+
super.visitChildrenForSemantics(visitor);
550+
}
551+
}
552+
553+
@override
554+
void paint(PaintingContext context, Offset offset) {
555+
if (!visible) {
556+
return;
557+
}
558+
super.paint(context, offset);
559+
}
560+
}
561+
562+
// A widget that conditionally hides its child, but without the forced compositing of `SliverOpacity`.
563+
//
564+
// A fully opaque `SliverOpacity` widget is required to leave its opacity layer in the layer tree.
565+
// This forces all parent render objects to also composite, which can break a simple scene into many
566+
// different layers. This can be significantly more expensive, so the issue is avoided by a
567+
// specialized render object that does not ever force compositing.
568+
class _SliverVisibility extends SingleChildRenderObjectWidget {
569+
const _SliverVisibility({ required this.visible, required this.maintainSemantics, Widget? sliver })
570+
: super(child: sliver);
571+
572+
final bool visible;
573+
final bool maintainSemantics;
574+
575+
@override
576+
RenderObject createRenderObject(BuildContext context) {
577+
return _RenderSliverVisibility(visible, maintainSemantics);
578+
}
579+
580+
@override
581+
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) {
582+
(renderObject as _RenderSliverVisibility)
583+
..visible = visible
584+
..maintainSemantics = maintainSemantics;
585+
}
586+
}
587+
588+
class _RenderSliverVisibility extends RenderProxySliver {
589+
_RenderSliverVisibility(this._visible, this._maintainSemantics);
590+
591+
bool get visible => _visible;
592+
bool _visible;
593+
set visible(bool value) {
594+
if (value == visible) {
595+
return;
596+
}
597+
_visible = value;
598+
markNeedsPaint();
599+
}
600+
601+
bool get maintainSemantics => _maintainSemantics;
602+
bool _maintainSemantics;
603+
set maintainSemantics(bool value) {
604+
if (value == maintainSemantics) {
605+
return;
606+
}
607+
_maintainSemantics = value;
608+
markNeedsSemanticsUpdate();
609+
}
610+
611+
@override
612+
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
613+
if (maintainSemantics || visible) {
614+
super.visitChildrenForSemantics(visitor);
615+
}
616+
}
617+
618+
@override
619+
void paint(PaintingContext context, Offset offset) {
620+
if (!visible) {
621+
return;
622+
}
623+
super.paint(context, offset);
624+
}
625+
}

packages/flutter/test/widgets/visibility_test.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/rendering.dart';
56
import 'package:flutter/widgets.dart';
67
import 'package:flutter_test/flutter_test.dart';
78

@@ -424,4 +425,50 @@ void main() {
424425

425426
semantics.dispose();
426427
});
428+
429+
testWidgets('Visibility does not force compositing when visible and maintain*', (WidgetTester tester) async {
430+
await tester.pumpWidget(
431+
const Visibility(
432+
maintainSize: true,
433+
maintainAnimation: true,
434+
maintainState: true,
435+
child: Text('hello', textDirection: TextDirection.ltr),
436+
),
437+
);
438+
439+
// Root transform from the tester and then the picture created by the text.
440+
expect(tester.layers, hasLength(2));
441+
expect(tester.layers, isNot(contains(isA<OpacityLayer>())));
442+
expect(tester.layers.last, isA<PictureLayer>());
443+
});
444+
445+
testWidgets('SliverVisibility does not force compositing when visible and maintain*', (WidgetTester tester) async {
446+
await tester.pumpWidget(
447+
const Directionality(
448+
textDirection: TextDirection.ltr,
449+
child: CustomScrollView(
450+
slivers: <Widget>[
451+
SliverVisibility(
452+
maintainSize: true,
453+
maintainAnimation: true,
454+
maintainState: true,
455+
sliver: SliverList(
456+
delegate: SliverChildListDelegate.fixed(
457+
addRepaintBoundaries: false,
458+
<Widget>[
459+
Text('hello'),
460+
],
461+
),
462+
))
463+
]
464+
),
465+
),
466+
);
467+
468+
// This requires a lot more layers due to including sliver lists which do manage additional
469+
// offset layers. Just trust me this is one fewer layers than before...
470+
expect(tester.layers, hasLength(6));
471+
expect(tester.layers, isNot(contains(isA<OpacityLayer>())));
472+
expect(tester.layers.last, isA<PictureLayer>());
473+
});
427474
}

0 commit comments

Comments
 (0)