Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
0cb5772e
Unverified
Commit
0cb5772e
authored
Apr 26, 2022
by
Jonah Williams
Committed by
GitHub
Apr 26, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[framework] allow other RenderObjects to behave like repaint boundaries (#101952)
parent
61bbaaa4
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
794 additions
and
58 deletions
+794
-58
object.dart
packages/flutter/lib/src/rendering/object.dart
+187
-18
proxy_box.dart
packages/flutter/lib/src/rendering/proxy_box.dart
+30
-34
heroes.dart
packages/flutter/lib/src/widgets/heroes.dart
+3
-5
debug_test.dart
packages/flutter/test/rendering/debug_test.dart
+2
-1
proxy_box_test.dart
packages/flutter/test/rendering/proxy_box_test.dart
+207
-0
animated_opacity_repaint_test.dart
...s/flutter/test/widgets/animated_opacity_repaint_test.dart
+90
-0
opacity_repaint_test.dart
packages/flutter/test/widgets/opacity_repaint_test.dart
+275
-0
No files found.
packages/flutter/lib/src/rendering/object.dart
View file @
0cb5772e
...
...
@@ -121,22 +121,38 @@ class PaintingContext extends ClipContext {
if
(
childLayer
==
null
)
{
assert
(
debugAlsoPaintedParent
);
assert
(
child
.
_layerHandle
.
layer
==
null
);
// Not using the `layer` setter because the setter asserts that we not
// replace the layer for repaint boundaries. That assertion does not
// apply here because this is exactly the place designed to create a
// layer for repaint boundaries.
final
OffsetLayer
layer
=
OffsetLayer
(
);
final
OffsetLayer
layer
=
child
.
updateCompositedLayer
(
oldLayer:
null
);
child
.
_layerHandle
.
layer
=
childLayer
=
layer
;
}
else
{
assert
(
debugAlsoPaintedParent
||
childLayer
.
attached
);
Offset
?
debugOldOffset
;
assert
(()
{
debugOldOffset
=
childLayer
!.
offset
;
return
true
;
}());
childLayer
.
removeAllChildren
();
final
OffsetLayer
updatedLayer
=
child
.
updateCompositedLayer
(
oldLayer:
childLayer
);
assert
(
identical
(
updatedLayer
,
childLayer
),
'
$child
created a new layer instance
$updatedLayer
instead of reusing the '
'existing layer
$childLayer
. See the documentation of RenderObject.updateCompositedLayer '
'for more information on how to correctly implement this method.'
);
assert
(
debugOldOffset
==
updatedLayer
.
offset
);
}
child
.
_needsCompositedLayerUpdate
=
false
;
assert
(
identical
(
childLayer
,
child
.
_layerHandle
.
layer
));
assert
(
child
.
_layerHandle
.
layer
is
OffsetLayer
);
assert
(()
{
childLayer
!.
debugCreator
=
child
.
debugCreator
??
child
.
runtimeType
;
return
true
;
}());
childContext
??=
PaintingContext
(
childLayer
,
child
.
paintBounds
);
child
.
_paintWithContext
(
childContext
,
Offset
.
zero
);
...
...
@@ -146,6 +162,38 @@ class PaintingContext extends ClipContext {
childContext
.
stopRecordingIfNeeded
();
}
/// Update the composited layer of [child] without repainting its children.
///
/// The render object must be attached to a [PipelineOwner], must have a
/// composited layer, and must be in need of a composited layer update but
/// not in need of painting. The render object's layer is re-used, and none
/// of its children are repaint or their layers updated.
///
/// See also:
///
/// * [RenderObject.isRepaintBoundary], which determines if a [RenderObject]
/// has a composited layer.
static
void
updateLayerProperties
(
RenderObject
child
)
{
assert
(
child
.
isRepaintBoundary
&&
child
.
_wasRepaintBoundary
);
assert
(!
child
.
_needsPaint
);
assert
(
child
.
_layerHandle
.
layer
!=
null
);
final
OffsetLayer
childLayer
=
child
.
_layerHandle
.
layer
!
as
OffsetLayer
;
Offset
?
debugOldOffset
;
assert
(()
{
debugOldOffset
=
childLayer
.
offset
;
return
true
;
}());
final
OffsetLayer
updatedLayer
=
child
.
updateCompositedLayer
(
oldLayer:
childLayer
);
assert
(
identical
(
updatedLayer
,
childLayer
),
'
$child
created a new layer instance
$updatedLayer
instead of reusing the '
'existing layer
$childLayer
. See the documentation of RenderObject.updateCompositedLayer '
'for more information on how to correctly implement this method.'
);
assert
(
debugOldOffset
==
updatedLayer
.
offset
);
child
.
_needsCompositedLayerUpdate
=
false
;
}
/// In debug mode, repaint the given render object using a custom painting
/// context that can record the results of the painting operation in addition
/// to performing the regular paint of the child.
...
...
@@ -183,6 +231,12 @@ class PaintingContext extends ClipContext {
if
(
child
.
isRepaintBoundary
)
{
stopRecordingIfNeeded
();
_compositeChild
(
child
,
offset
);
// If a render object was a repaint boundary but no longer is one, this
// is where the framework managed layer is automatically disposed.
}
else
if
(
child
.
_wasRepaintBoundary
)
{
assert
(
child
.
_layerHandle
.
layer
is
OffsetLayer
);
child
.
_layerHandle
.
layer
=
null
;
child
.
_paintWithContext
(
this
,
offset
);
}
else
{
child
.
_paintWithContext
(
this
,
offset
);
}
...
...
@@ -194,9 +248,12 @@ class PaintingContext extends ClipContext {
assert
(
_canvas
==
null
||
_canvas
!.
getSaveCount
()
==
1
);
// Create a layer for our child, and paint the child into it.
if
(
child
.
_needsPaint
)
{
if
(
child
.
_needsPaint
||
!
child
.
_wasRepaintBoundary
)
{
repaintCompositedChild
(
child
,
debugAlsoPaintedParent:
true
);
}
else
{
if
(
child
.
_needsCompositedLayerUpdate
)
{
updateLayerProperties
(
child
);
}
assert
(()
{
// register the call for RepaintBoundary metrics
child
.
debugRegisterRepaintBoundaryPaint
();
...
...
@@ -978,19 +1035,25 @@ class PipelineOwner {
arguments:
debugTimelineArguments
,
);
}
assert
(()
{
_debugDoingPaint
=
true
;
return
true
;
}());
try
{
assert
(()
{
_debugDoingPaint
=
true
;
return
true
;
}());
final
List
<
RenderObject
>
dirtyNodes
=
_nodesNeedingPaint
;
_nodesNeedingPaint
=
<
RenderObject
>[];
// Sort the dirty nodes in reverse order (deepest first).
for
(
final
RenderObject
node
in
dirtyNodes
..
sort
((
RenderObject
a
,
RenderObject
b
)
=>
b
.
depth
-
a
.
depth
))
{
assert
(
node
.
_layerHandle
.
layer
!=
null
);
if
(
node
.
_needsPaint
&&
node
.
owner
==
this
)
{
if
(
(
node
.
_needsPaint
||
node
.
_needsCompositedLayerUpdate
)
&&
node
.
owner
==
this
)
{
if
(
node
.
_layerHandle
.
layer
!.
attached
)
{
PaintingContext
.
repaintCompositedChild
(
node
);
assert
(
node
.
isRepaintBoundary
);
if
(
node
.
_needsPaint
)
{
PaintingContext
.
repaintCompositedChild
(
node
);
}
else
{
PaintingContext
.
updateLayerProperties
(
node
);
}
}
else
{
node
.
_skippedPaintingOnLayer
();
}
...
...
@@ -1236,6 +1299,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// Initializes internal fields for subclasses.
RenderObject
()
{
_needsCompositing
=
isRepaintBoundary
||
alwaysNeedsCompositing
;
_wasRepaintBoundary
=
isRepaintBoundary
;
}
/// Cause the entire subtree rooted at the given [RenderObject] to be marked
...
...
@@ -2070,12 +2134,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// to repaint.
///
/// If this getter returns true, the [paintBounds] are applied to this object
/// and all descendants. The framework
automatically creates an [Offset
Layer]
///
and assigns it to the [layer] field. Render objects that declare
///
themselves as repaint boundaries must not replace the layer created by
/// the framework.
/// and all descendants. The framework
invokes [RenderObject.updateComposited
Layer]
///
to create an [OffsetLayer] and assigns it to the [layer] field.
///
Render objects that declare themselves as repaint boundaries must not replace
/// the
layer created by the
framework.
///
/// Warning: This getter must not change value over the lifetime of this object.
/// If the value of this getter changes, [markNeedsCompositingBitsUpdate] must
/// be called.
///
/// See [RepaintBoundary] for more information about how repaint boundaries function.
bool
get
isRepaintBoundary
=>
false
;
...
...
@@ -2098,6 +2163,34 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
@protected
bool
get
alwaysNeedsCompositing
=>
false
;
late
bool
_wasRepaintBoundary
;
/// Update the composited layer owned by this render object.
///
/// This method is called by the framework when [isRepaintBoundary] is true.
///
/// If [oldLayer] is `null`, this method must return a new [OffsetLayer]
/// (or subtype thereof). If [oldLayer] is not `null`, then this method must
/// reuse the layer instance that is provided - it is an error to create a new
/// layer in this instance. The layer will be disposed by the framework when
/// either the render object is disposed or if it is no longer a repaint
/// boundary.
///
/// The [OffsetLayer.offset] property will be managed by the framework and
/// must not be updated by this method.
///
/// If a property of the composited layer needs to be updated, the render object
/// must call [markNeedsCompositedLayerUpdate] which will schedule this method
/// to be called without repainting children. If this widget was marked as
/// needing to paint and needing a composited layer update, this method is only
/// called once.
// TODO(jonahwilliams): https://github.com/flutter/flutter/issues/102102 revisit the
// contraint that the instance/type of layer cannot be changed at runtime.
OffsetLayer
updateCompositedLayer
({
required
covariant
OffsetLayer
?
oldLayer
})
{
assert
(
isRepaintBoundary
);
return
oldLayer
??
OffsetLayer
();
}
/// The compositing layer that this render object uses to repaint.
///
/// If this render object is not a repaint boundary, it is the responsibility
...
...
@@ -2184,7 +2277,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
final
RenderObject
parent
=
this
.
parent
!
as
RenderObject
;
if
(
parent
.
_needsCompositingBitsUpdate
)
return
;
if
(!
isRepaintBoundary
&&
!
parent
.
isRepaintBoundary
)
{
if
((!
_wasRepaintBoundary
||
!
isRepaintBoundary
)
&&
!
parent
.
isRepaintBoundary
)
{
parent
.
markNeedsCompositingBitsUpdate
();
return
;
}
...
...
@@ -2225,9 +2319,23 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
});
if
(
isRepaintBoundary
||
alwaysNeedsCompositing
)
_needsCompositing
=
true
;
if
(
oldNeedsCompositing
!=
_needsCompositing
)
// If a node was previously a repaint boundary, but no longer is one, then
// regardless of its compositing state we need to find a new parent to
// paint from. To do this, we mark it clean again so that the traversal
// in markNeedsPaint is not short-circuited. It is removed from _nodesNeedingPaint
// so that we do not attempt to paint from it after locating a parent.
if
(!
isRepaintBoundary
&&
_wasRepaintBoundary
)
{
_needsPaint
=
false
;
_needsCompositedLayerUpdate
=
false
;
owner
?.
_nodesNeedingPaint
.
remove
(
this
);
_needsCompositingBitsUpdate
=
false
;
markNeedsPaint
();
}
else
if
(
oldNeedsCompositing
!=
_needsCompositing
)
{
_needsCompositingBitsUpdate
=
false
;
markNeedsPaint
();
_needsCompositingBitsUpdate
=
false
;
}
else
{
_needsCompositingBitsUpdate
=
false
;
}
}
/// Whether this render object's paint information is dirty.
...
...
@@ -2254,6 +2362,24 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
}
bool
_needsPaint
=
true
;
/// Whether this render object's layer information is dirty.
///
/// This is only set in debug mode. In general, render objects should not need
/// to condition their runtime behavior on whether they are dirty or not,
/// since they should only be marked dirty immediately prior to being laid
/// out and painted. (In release builds, this throws.)
///
/// It is intended to be used by tests and asserts.
bool
get
debugNeedsCompositedLayerUpdate
{
late
bool
result
;
assert
(()
{
result
=
_needsCompositedLayerUpdate
;
return
true
;
}());
return
result
;
}
bool
_needsCompositedLayerUpdate
=
false
;
/// Mark this render object as having changed its visual appearance.
///
/// Rather than eagerly updating this render object's display list
...
...
@@ -2280,7 +2406,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
if
(
_needsPaint
)
return
;
_needsPaint
=
true
;
if
(
isRepaintBoundary
)
{
// If this was not previously a repaint boundary it will not have
// a layer we can paint from.
if
(
isRepaintBoundary
&&
_wasRepaintBoundary
)
{
assert
(()
{
if
(
debugPrintMarkNeedsPaintStacks
)
debugPrintStack
(
label:
'markNeedsPaint() called for
$this
'
);
...
...
@@ -2312,6 +2440,45 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
}
}
/// Mark this render object as having changed a property on its composited
/// layer.
///
/// Render objects that have a composited layer have [isRepaintBoundary] equal
/// to true may update the properties of that composited layer without repainting
/// their children. If this render object is a repaint boundary but does
/// not yet have a composited layer created for it, this method will instead
/// mark the nearest repaint boundary parent as needing to be painted.
///
/// If this method is called on a render object that is not a repaint boundary
/// or is a repaint boundary but hasn't been composited yet, it is equivalent
/// to calling [markNeedsPaint].
///
/// See also:
///
/// * [RenderOpacity], which uses this method when its opacity is updated to
/// update the layer opacity without repainting children.
void
markNeedsCompositedLayerUpdate
()
{
assert
(!
_debugDisposed
);
assert
(
owner
==
null
||
!
owner
!.
debugDoingPaint
);
if
(
_needsCompositedLayerUpdate
||
_needsPaint
)
{
return
;
}
_needsCompositedLayerUpdate
=
true
;
// If this was not previously a repaint boundary it will not have
// a layer we can paint from.
if
(
isRepaintBoundary
&&
_wasRepaintBoundary
)
{
// If we always have our own layer, then we can just repaint
// ourselves without involving any other nodes.
assert
(
_layerHandle
.
layer
!=
null
);
if
(
owner
!=
null
)
{
owner
!.
_nodesNeedingPaint
.
add
(
this
);
owner
!.
requestVisualUpdate
();
}
}
else
{
markNeedsPaint
();
}
}
// Called when flushPaint() tries to make us paint but our layer is detached.
// To make sure that our subtree is repainted when it's finally reattached,
// even in the case where some ancestor layer is itself never marked dirty, we
...
...
@@ -2320,7 +2487,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
void
_skippedPaintingOnLayer
()
{
assert
(
attached
);
assert
(
isRepaintBoundary
);
assert
(
_needsPaint
);
assert
(
_needsPaint
||
_needsCompositedLayerUpdate
);
assert
(
_layerHandle
.
layer
!=
null
);
assert
(!
_layerHandle
.
layer
!.
attached
);
AbstractNode
?
node
=
parent
;
...
...
@@ -2475,6 +2642,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
return
true
;
}());
_needsPaint
=
false
;
_needsCompositedLayerUpdate
=
false
;
_wasRepaintBoundary
=
isRepaintBoundary
;
try
{
paint
(
context
,
offset
);
assert
(!
_needsLayout
);
// check that the paint() method didn't mark us dirty again
...
...
packages/flutter/lib/src/rendering/proxy_box.dart
View file @
0cb5772e
...
...
@@ -843,7 +843,14 @@ class RenderOpacity extends RenderProxyBox {
super
(
child
);
@override
bool
get
alwaysNeedsCompositing
=>
child
!=
null
&&
(
_alpha
>
0
);
bool
get
isRepaintBoundary
=>
child
!=
null
&&
(
_alpha
>
0
);
@override
OffsetLayer
updateCompositedLayer
({
required
covariant
OpacityLayer
?
oldLayer
})
{
final
OpacityLayer
updatedLayer
=
oldLayer
??
OpacityLayer
();
updatedLayer
.
alpha
=
_alpha
;
return
updatedLayer
;
}
int
_alpha
;
...
...
@@ -864,13 +871,13 @@ class RenderOpacity extends RenderProxyBox {
assert
(
value
>=
0.0
&&
value
<=
1.0
);
if
(
_opacity
==
value
)
return
;
final
bool
didNeedCompositing
=
alwaysNeedsCompositing
;
final
bool
wasRepaintBoundary
=
isRepaintBoundary
;
final
bool
wasVisible
=
_alpha
!=
0
;
_opacity
=
value
;
_alpha
=
ui
.
Color
.
getAlphaFromOpacity
(
_opacity
);
if
(
didNeedCompositing
!=
alwaysNeedsCompositing
)
if
(
wasRepaintBoundary
!=
isRepaintBoundary
)
markNeedsCompositingBitsUpdate
();
markNeeds
Paint
();
markNeeds
CompositedLayerUpdate
();
if
(
wasVisible
!=
(
_alpha
!=
0
)
&&
!
alwaysIncludeSemantics
)
markNeedsSemanticsUpdate
();
}
...
...
@@ -891,19 +898,10 @@ class RenderOpacity extends RenderProxyBox {
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
if
(
child
!=
null
)
{
if
(
_alpha
==
0
)
{
// No need to keep the layer. We'll create a new one if necessary.
layer
=
null
;
return
;
}
assert
(
needsCompositing
);
layer
=
context
.
pushOpacity
(
offset
,
_alpha
,
super
.
paint
,
oldLayer:
layer
as
OpacityLayer
?);
assert
(()
{
layer
!.
debugCreator
=
debugCreator
;
return
true
;
}());
if
(
_alpha
==
0
)
{
return
;
}
super
.
paint
(
context
,
offset
);
}
@override
...
...
@@ -929,8 +927,15 @@ mixin RenderAnimatedOpacityMixin<T extends RenderObject> on RenderObjectWithChil
int
?
_alpha
;
@override
bool
get
alwaysNeedsCompositing
=>
child
!=
null
&&
_currentlyNeedsCompositing
!;
bool
?
_currentlyNeedsCompositing
;
bool
get
isRepaintBoundary
=>
child
!=
null
&&
_currentlyIsRepaintBoundary
!;
bool
?
_currentlyIsRepaintBoundary
;
@override
OffsetLayer
updateCompositedLayer
({
required
covariant
OpacityLayer
?
oldLayer
})
{
final
OpacityLayer
updatedLayer
=
oldLayer
??
OpacityLayer
();
updatedLayer
.
alpha
=
_alpha
;
return
updatedLayer
;
}
/// The animation that drives this render object's opacity.
///
...
...
@@ -990,11 +995,11 @@ mixin RenderAnimatedOpacityMixin<T extends RenderObject> on RenderObjectWithChil
final
int
?
oldAlpha
=
_alpha
;
_alpha
=
ui
.
Color
.
getAlphaFromOpacity
(
opacity
.
value
);
if
(
oldAlpha
!=
_alpha
)
{
final
bool
?
didNeedCompositing
=
_currentlyNeedsCompositing
;
_currently
NeedsCompositing
=
_alpha
!
>
0
;
if
(
child
!=
null
&&
didNeedCompositing
!=
_currentlyNeedsCompositing
)
final
bool
?
wasRepaintBoundary
=
_currentlyIsRepaintBoundary
;
_currently
IsRepaintBoundary
=
_alpha
!
>
0
;
if
(
child
!=
null
&&
wasRepaintBoundary
!=
_currentlyIsRepaintBoundary
)
markNeedsCompositingBitsUpdate
();
markNeeds
Paint
();
markNeeds
CompositedLayerUpdate
();
if
(
oldAlpha
==
0
||
_alpha
==
0
)
markNeedsSemanticsUpdate
();
}
...
...
@@ -1002,19 +1007,10 @@ mixin RenderAnimatedOpacityMixin<T extends RenderObject> on RenderObjectWithChil
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
if
(
child
!=
null
)
{
if
(
_alpha
==
0
)
{
// No need to keep the layer. We'll create a new one if necessary.
layer
=
null
;
return
;
}
assert
(
needsCompositing
);
layer
=
context
.
pushOpacity
(
offset
,
_alpha
!,
super
.
paint
,
oldLayer:
layer
as
OpacityLayer
?);
assert
(()
{
layer
!.
debugCreator
=
debugCreator
;
return
true
;
}());
if
(
_alpha
==
0
)
{
return
;
}
super
.
paint
(
context
,
offset
);
}
@override
...
...
packages/flutter/lib/src/widgets/heroes.dart
View file @
0cb5772e
...
...
@@ -545,11 +545,9 @@ class _HeroFlight {
bottom:
offsets
.
bottom
,
left:
offsets
.
left
,
child:
IgnorePointer
(
child:
RepaintBoundary
(
child:
FadeTransition
(
opacity:
_heroOpacity
,
child:
child
,
),
child:
FadeTransition
(
opacity:
_heroOpacity
,
child:
child
,
),
),
);
...
...
packages/flutter/test/rendering/debug_test.dart
View file @
0cb5772e
...
...
@@ -231,7 +231,8 @@ void main() {
rootLayer
,
const
Rect
.
fromLTWH
(
0
,
0
,
500
,
500
),
);
root
.
paint
(
context
,
const
Offset
(
40
,
40
));
context
.
paintChild
(
root
,
const
Offset
(
40
,
40
));
final
OpacityLayer
opacityLayer
=
rootLayer
.
firstChild
!
as
OpacityLayer
;
expect
(
opacityLayer
.
offset
,
const
Offset
(
40
,
40
));
debugDisableOpacityLayers
=
false
;
...
...
packages/flutter/test/rendering/proxy_box_test.dart
View file @
0cb5772e
...
...
@@ -561,6 +561,178 @@ void main() {
// The follower is still hit testable because there is a leader layer.
expect
(
follower
.
hitTest
(
hitTestResult
,
position:
Offset
.
zero
),
isTrue
);
});
test
(
'RenderObject can become a repaint boundary'
,
()
{
final
ConditionalRepaintBoundary
childBox
=
ConditionalRepaintBoundary
();
final
ConditionalRepaintBoundary
renderBox
=
ConditionalRepaintBoundary
(
child:
childBox
);
layout
(
renderBox
,
phase:
EnginePhase
.
composite
);
expect
(
childBox
.
paintCount
,
1
);
expect
(
renderBox
.
paintCount
,
1
);
renderBox
.
isRepaintBoundary
=
true
;
renderBox
.
markNeedsCompositingBitsUpdate
();
renderBox
.
markNeedsCompositedLayerUpdate
();
pumpFrame
(
phase:
EnginePhase
.
composite
);
// The first time the render object becomes a repaint boundary
// we must repaint from the parent to allow the layer to be
// created.
expect
(
childBox
.
paintCount
,
2
);
expect
(
renderBox
.
paintCount
,
2
);
expect
(
renderBox
.
debugLayer
,
isA
<
OffsetLayer
>());
renderBox
.
markNeedsCompositedLayerUpdate
();
expect
(
renderBox
.
debugNeedsPaint
,
false
);
expect
(
renderBox
.
debugNeedsCompositedLayerUpdate
,
true
);
pumpFrame
(
phase:
EnginePhase
.
composite
);
// The second time the layer exists and we can skip paint.
expect
(
childBox
.
paintCount
,
2
);
expect
(
renderBox
.
paintCount
,
2
);
expect
(
renderBox
.
debugLayer
,
isA
<
OffsetLayer
>());
renderBox
.
isRepaintBoundary
=
false
;
renderBox
.
markNeedsCompositingBitsUpdate
();
pumpFrame
(
phase:
EnginePhase
.
composite
);
// Once it stops being a repaint boundary we must repaint to
// remove the layer. its required that the render object
// perform this action in paint.
expect
(
childBox
.
paintCount
,
3
);
expect
(
renderBox
.
paintCount
,
3
);
expect
(
renderBox
.
debugLayer
,
null
);
// When the render object is not a repaint boundary, calling
// markNeedsLayerPropertyUpdate is the same as calling
// markNeedsPaint.
renderBox
.
markNeedsCompositedLayerUpdate
();
expect
(
renderBox
.
debugNeedsPaint
,
true
);
expect
(
renderBox
.
debugNeedsCompositedLayerUpdate
,
true
);
});
test
(
'RenderObject with repaint boundary asserts when a composited layer is replaced during layer property update'
,
()
{
final
ConditionalRepaintBoundary
childBox
=
ConditionalRepaintBoundary
(
isRepaintBoundary:
true
);
final
ConditionalRepaintBoundary
renderBox
=
ConditionalRepaintBoundary
(
child:
childBox
);
// Ignore old layer.
childBox
.
offsetLayerFactory
=
(
OffsetLayer
?
oldLayer
)
{
return
TestOffsetLayerA
();
};
layout
(
renderBox
,
phase:
EnginePhase
.
composite
);
expect
(
childBox
.
paintCount
,
1
);
expect
(
renderBox
.
paintCount
,
1
);
renderBox
.
markNeedsCompositedLayerUpdate
();
pumpFrame
(
phase:
EnginePhase
.
composite
,
onErrors:
expectAssertionError
);
},
skip:
kIsWeb
);
// https://github.com/flutter/flutter/issues/102086
test
(
'RenderObject with repaint boundary asserts when a composited layer is replaced during painting'
,
()
{
final
ConditionalRepaintBoundary
childBox
=
ConditionalRepaintBoundary
(
isRepaintBoundary:
true
);
final
ConditionalRepaintBoundary
renderBox
=
ConditionalRepaintBoundary
(
child:
childBox
);
// Ignore old layer.
childBox
.
offsetLayerFactory
=
(
OffsetLayer
?
oldLayer
)
{
return
TestOffsetLayerA
();
};
layout
(
renderBox
,
phase:
EnginePhase
.
composite
);
expect
(
childBox
.
paintCount
,
1
);
expect
(
renderBox
.
paintCount
,
1
);
renderBox
.
markNeedsPaint
();
pumpFrame
(
phase:
EnginePhase
.
composite
,
onErrors:
expectAssertionError
);
},
skip:
kIsWeb
);
// https://github.com/flutter/flutter/issues/102086
test
(
'RenderObject with repaint boundary asserts when a composited layer tries to update its own offset'
,
()
{
final
ConditionalRepaintBoundary
childBox
=
ConditionalRepaintBoundary
(
isRepaintBoundary:
true
);
final
ConditionalRepaintBoundary
renderBox
=
ConditionalRepaintBoundary
(
child:
childBox
);
// Ignore old layer.
childBox
.
offsetLayerFactory
=
(
OffsetLayer
?
oldLayer
)
{
return
(
oldLayer
??
TestOffsetLayerA
())..
offset
=
const
Offset
(
2133
,
4422
);
};
layout
(
renderBox
,
phase:
EnginePhase
.
composite
);
expect
(
childBox
.
paintCount
,
1
);
expect
(
renderBox
.
paintCount
,
1
);
renderBox
.
markNeedsPaint
();
pumpFrame
(
phase:
EnginePhase
.
composite
,
onErrors:
expectAssertionError
);
},
skip:
kIsWeb
);
// https://github.com/flutter/flutter/issues/102086
test
(
'RenderObject markNeedsPaint while repaint boundary, and then updated to no longer be a repaint boundary with '
'calling markNeedsCompositingBitsUpdate 1'
,
()
{
final
ConditionalRepaintBoundary
childBox
=
ConditionalRepaintBoundary
(
isRepaintBoundary:
true
);
final
ConditionalRepaintBoundary
renderBox
=
ConditionalRepaintBoundary
(
child:
childBox
);
// Ignore old layer.
childBox
.
offsetLayerFactory
=
(
OffsetLayer
?
oldLayer
)
{
return
oldLayer
??
TestOffsetLayerA
();
};
layout
(
renderBox
,
phase:
EnginePhase
.
composite
);
expect
(
childBox
.
paintCount
,
1
);
expect
(
renderBox
.
paintCount
,
1
);
childBox
.
markNeedsPaint
();
childBox
.
isRepaintBoundary
=
false
;
childBox
.
markNeedsCompositingBitsUpdate
();
expect
(()
=>
pumpFrame
(
phase:
EnginePhase
.
composite
),
returnsNormally
);
});
test
(
'RenderObject markNeedsPaint while repaint boundary, and then updated to no longer be a repaint boundary with '
'calling markNeedsCompositingBitsUpdate 2'
,
()
{
final
ConditionalRepaintBoundary
childBox
=
ConditionalRepaintBoundary
(
isRepaintBoundary:
true
);
final
ConditionalRepaintBoundary
renderBox
=
ConditionalRepaintBoundary
(
child:
childBox
);
// Ignore old layer.
childBox
.
offsetLayerFactory
=
(
OffsetLayer
?
oldLayer
)
{
return
oldLayer
??
TestOffsetLayerA
();
};
layout
(
renderBox
,
phase:
EnginePhase
.
composite
);
expect
(
childBox
.
paintCount
,
1
);
expect
(
renderBox
.
paintCount
,
1
);
childBox
.
isRepaintBoundary
=
false
;
childBox
.
markNeedsCompositingBitsUpdate
();
childBox
.
markNeedsPaint
();
expect
(()
=>
pumpFrame
(
phase:
EnginePhase
.
composite
),
returnsNormally
);
});
test
(
'RenderObject markNeedsPaint while repaint boundary, and then updated to no longer be a repaint boundary with '
'calling markNeedsCompositingBitsUpdate 3'
,
()
{
final
ConditionalRepaintBoundary
childBox
=
ConditionalRepaintBoundary
(
isRepaintBoundary:
true
);
final
ConditionalRepaintBoundary
renderBox
=
ConditionalRepaintBoundary
(
child:
childBox
);
// Ignore old layer.
childBox
.
offsetLayerFactory
=
(
OffsetLayer
?
oldLayer
)
{
return
oldLayer
??
TestOffsetLayerA
();
};
layout
(
renderBox
,
phase:
EnginePhase
.
composite
);
expect
(
childBox
.
paintCount
,
1
);
expect
(
renderBox
.
paintCount
,
1
);
childBox
.
isRepaintBoundary
=
false
;
childBox
.
markNeedsCompositedLayerUpdate
();
childBox
.
markNeedsCompositingBitsUpdate
();
expect
(()
=>
pumpFrame
(
phase:
EnginePhase
.
composite
),
returnsNormally
);
});
}
class
_TestRectClipper
extends
CustomClipper
<
Rect
>
{
...
...
@@ -631,3 +803,38 @@ class _TestSemanticsUpdateRenderFractionalTranslation extends RenderFractionalTr
super
.
markNeedsSemanticsUpdate
();
}
}
class
ConditionalRepaintBoundary
extends
RenderProxyBox
{
ConditionalRepaintBoundary
({
this
.
isRepaintBoundary
=
false
,
RenderBox
?
child
})
:
super
(
child
);
@override
bool
isRepaintBoundary
=
false
;
OffsetLayer
Function
(
OffsetLayer
?)?
offsetLayerFactory
;
int
paintCount
=
0
;
@override
OffsetLayer
updateCompositedLayer
({
required
covariant
OffsetLayer
?
oldLayer
})
{
if
(
offsetLayerFactory
!=
null
)
{
return
offsetLayerFactory
!.
call
(
oldLayer
);
}
return
super
.
updateCompositedLayer
(
oldLayer:
oldLayer
);
}
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
paintCount
+=
1
;
super
.
paint
(
context
,
offset
);
}
}
class
TestOffsetLayerA
extends
OffsetLayer
{}
void
expectAssertionError
(
)
{
final
FlutterErrorDetails
errorDetails
=
TestRenderingFlutterBinding
.
instance
.
takeFlutterErrorDetails
()!;
final
bool
asserted
=
errorDetails
.
toString
().
contains
(
'Failed assertion'
);
if
(!
asserted
)
{
FlutterError
.
reportError
(
errorDetails
);
}
}
packages/flutter/test/widgets/animated_opacity_repaint_test.dart
0 → 100644
View file @
0cb5772e
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/material.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
testWidgets
(
'RenderAnimatedOpacityMixin avoids repainting child as it animates'
,
(
WidgetTester
tester
)
async
{
RenderTestObject
.
paintCount
=
0
;
final
AnimationController
controller
=
AnimationController
(
vsync:
const
TestVSync
(),
duration:
const
Duration
(
seconds:
1
));
final
Tween
<
double
>
opacityTween
=
Tween
<
double
>(
begin:
0
,
end:
1
);
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
red
,
child:
FadeTransition
(
opacity:
controller
.
drive
(
opacityTween
),
child:
const
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
0
);
controller
.
forward
();
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
RenderTestObject
.
paintCount
,
1
);
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
500
));
expect
(
RenderTestObject
.
paintCount
,
1
);
controller
.
stop
();
await
tester
.
pump
();
expect
(
RenderTestObject
.
paintCount
,
1
);
});
testWidgets
(
'RenderAnimatedOpacityMixin allows opacity layer to be disposed when animating to 0 opacity'
,
(
WidgetTester
tester
)
async
{
RenderTestObject
.
paintCount
=
0
;
final
AnimationController
controller
=
AnimationController
(
vsync:
const
TestVSync
(),
duration:
const
Duration
(
seconds:
1
));
final
Tween
<
double
>
opacityTween
=
Tween
<
double
>(
begin:
1
,
end:
0
);
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
red
,
child:
FadeTransition
(
opacity:
controller
.
drive
(
opacityTween
),
child:
const
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
expect
(
tester
.
layers
,
contains
(
isA
<
OpacityLayer
>()));
controller
.
forward
();
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
seconds:
2
));
expect
(
RenderTestObject
.
paintCount
,
1
);
controller
.
stop
();
await
tester
.
pump
();
expect
(
tester
.
layers
,
isNot
(
contains
(
isA
<
OpacityLayer
>())));
});
}
class
TestWidget
extends
SingleChildRenderObjectWidget
{
const
TestWidget
({
super
.
key
,
super
.
child
});
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
RenderTestObject
();
}
}
class
RenderTestObject
extends
RenderProxyBox
{
static
int
paintCount
=
0
;
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
paintCount
+=
1
;
super
.
paint
(
context
,
offset
);
}
}
packages/flutter/test/widgets/opacity_repaint_test.dart
0 → 100644
View file @
0cb5772e
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/material.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
testWidgets
(
'RenderOpacity acts as a repaint boundary for changes above the widget when partially opaque'
,
(
WidgetTester
tester
)
async
{
RenderTestObject
.
paintCount
=
0
;
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
red
,
child:
const
Opacity
(
opacity:
0.5
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
blue
,
child:
const
Opacity
(
opacity:
0.5
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
});
testWidgets
(
'RenderOpacity acts as a repaint boundary for changes above the widget when fully opaque'
,
(
WidgetTester
tester
)
async
{
RenderTestObject
.
paintCount
=
0
;
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
red
,
child:
const
Opacity
(
opacity:
1
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
blue
,
child:
const
Opacity
(
opacity:
1
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
});
testWidgets
(
'RenderOpacity can update its opacity without repainting its child - partially opaque to partially opaque'
,
(
WidgetTester
tester
)
async
{
RenderTestObject
.
paintCount
=
0
;
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
red
,
child:
const
Opacity
(
opacity:
0.5
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
blue
,
child:
const
Opacity
(
opacity:
0.9
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
});
testWidgets
(
'RenderOpacity can update its opacity without repainting its child - partially opaque to fully opaque'
,
(
WidgetTester
tester
)
async
{
RenderTestObject
.
paintCount
=
0
;
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
red
,
child:
const
Opacity
(
opacity:
0.5
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
blue
,
child:
const
Opacity
(
opacity:
1
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
});
testWidgets
(
'RenderOpacity can update its opacity without repainting its child - fully opaque to partially opaque'
,
(
WidgetTester
tester
)
async
{
RenderTestObject
.
paintCount
=
0
;
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
red
,
child:
const
Opacity
(
opacity:
1
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
blue
,
child:
const
Opacity
(
opacity:
0.5
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
});
testWidgets
(
'RenderOpacity can update its opacity without repainting its child - fully opaque to fully transparent'
,
(
WidgetTester
tester
)
async
{
RenderTestObject
.
paintCount
=
0
;
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
red
,
child:
const
Opacity
(
opacity:
1
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
blue
,
child:
const
Opacity
(
opacity:
0
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
});
testWidgets
(
'RenderOpacity must paint child - fully transparent to partially opaque'
,
(
WidgetTester
tester
)
async
{
RenderTestObject
.
paintCount
=
0
;
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
red
,
child:
const
Opacity
(
opacity:
0
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
0
);
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
blue
,
child:
const
Opacity
(
opacity:
0.5
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
});
testWidgets
(
'RenderOpacity allows child to update without updating parent'
,
(
WidgetTester
tester
)
async
{
RenderTestObject
.
paintCount
=
0
;
await
tester
.
pumpWidget
(
TestWidget
(
child:
Opacity
(
opacity:
0.5
,
child:
Container
(
color:
Colors
.
red
,
),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
await
tester
.
pumpWidget
(
TestWidget
(
child:
Opacity
(
opacity:
0.5
,
child:
Container
(
color:
Colors
.
blue
,
),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
});
testWidgets
(
'RenderOpacity disposes of opacity layer when opacity is updated to 0'
,
(
WidgetTester
tester
)
async
{
RenderTestObject
.
paintCount
=
0
;
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
red
,
child:
const
Opacity
(
opacity:
0.5
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
expect
(
tester
.
layers
,
contains
(
isA
<
OpacityLayer
>()));
await
tester
.
pumpWidget
(
Container
(
color:
Colors
.
blue
,
child:
const
Opacity
(
opacity:
0
,
child:
TestWidget
(),
),
)
);
expect
(
RenderTestObject
.
paintCount
,
1
);
expect
(
tester
.
layers
,
isNot
(
contains
(
isA
<
OpacityLayer
>())));
});
}
class
TestWidget
extends
SingleChildRenderObjectWidget
{
const
TestWidget
({
super
.
key
,
super
.
child
});
@override
RenderObject
createRenderObject
(
BuildContext
context
)
{
return
RenderTestObject
();
}
}
class
RenderTestObject
extends
RenderProxyBox
{
static
int
paintCount
=
0
;
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
paintCount
+=
1
;
super
.
paint
(
context
,
offset
);
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment