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
d2790bd2
Unverified
Commit
d2790bd2
authored
Apr 10, 2019
by
Dan Field
Committed by
GitHub
Apr 10, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Check for invalid elevations (#30215)
* Check for invalid elevation usage in the layer tree
parent
83ddd988
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
903 additions
and
25 deletions
+903
-25
binding.dart
packages/flutter/lib/src/rendering/binding.dart
+11
-0
debug.dart
packages/flutter/lib/src/rendering/debug.dart
+38
-0
layer.dart
packages/flutter/lib/src/rendering/layer.dart
+150
-3
proxy_box.dart
packages/flutter/lib/src/rendering/proxy_box.dart
+8
-0
service_extensions_test.dart
...ages/flutter/test/foundation/service_extensions_test.dart
+47
-22
layers_test.dart
packages/flutter/test/rendering/layers_test.dart
+217
-0
physical_model_test.dart
packages/flutter/test/widgets/physical_model_test.dart
+415
-0
resident_runner.dart
packages/flutter_tools/lib/src/resident_runner.dart
+15
-0
vmservice.dart
packages/flutter_tools/lib/src/vmservice.dart
+2
-0
No files found.
packages/flutter/lib/src/rendering/binding.dart
View file @
d2790bd2
...
...
@@ -87,6 +87,17 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
return
Future
<
void
>.
value
();
},
);
registerBoolServiceExtension
(
name:
'debugCheckElevationsEnabled'
,
getter:
()
async
=>
debugCheckElevationsEnabled
,
setter:
(
bool
value
)
{
if
(
debugCheckElevationsEnabled
==
value
)
{
return
Future
<
void
>.
value
();
}
debugCheckElevationsEnabled
=
value
;
return
_forceRepaint
();
}
);
registerSignalServiceExtension
(
name:
'debugDumpLayerTree'
,
callback:
()
{
...
...
packages/flutter/lib/src/rendering/debug.dart
View file @
d2790bd2
...
...
@@ -50,6 +50,44 @@ bool debugRepaintRainbowEnabled = false;
/// Overlay a rotating set of colors when repainting text in checked mode.
bool
debugRepaintTextRainbowEnabled
=
false
;
/// Causes [PhysicalModelLayer]s to paint a red rectangle around themselves if
/// they are overlapping and painted out of order with regard to their elevation.
///
/// Android and iOS will show the last painted layer on top, whereas Fuchsia
/// will show the layer with the highest elevation on top.
///
/// For example, a rectangular elevation at 3.0 that is painted before an
/// overlapping rectangular elevation at 2.0 would render this way on Android
/// and iOS (with fake shadows):
/// ```
/// ┌───────────────────┐
/// │ │
/// │ 3.0 │
/// │ ┌───────────────────┐
/// │ │ │
/// └────────────│ │
/// │ 2.0 │
/// │ │
/// └───────────────────┘
/// ```
///
/// But this way on Fuchsia (with real shadows):
/// ```
/// ┌───────────────────┐
/// │ │
/// │ 3.0 │
/// │ │────────────┐
/// │ │ │
/// └───────────────────┘ │
/// │ 2.0 │
/// │ │
/// └───────────────────┘
/// ```
///
/// This check helps developers that want a consistent look and feel detect
/// where this inconsistency would occur.
bool
debugCheckElevationsEnabled
=
false
;
/// The current color to overlay when repainting a layer.
///
/// This is used by painting debug code that implements
...
...
packages/flutter/lib/src/rendering/layer.dart
View file @
d2790bd2
...
...
@@ -4,7 +4,8 @@
import
'dart:async'
;
import
'dart:collection'
;
import
'dart:ui'
as
ui
show
EngineLayer
,
Image
,
ImageFilter
,
Picture
,
Scene
,
SceneBuilder
;
import
'dart:ui'
as
ui
show
EngineLayer
,
Image
,
ImageFilter
,
PathMetric
,
Picture
,
PictureRecorder
,
Scene
,
SceneBuilder
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/painting.dart'
;
...
...
@@ -503,6 +504,100 @@ class ContainerLayer extends Layer {
return
child
==
equals
;
}
PictureLayer
_highlightConflictingLayer
(
PhysicalModelLayer
child
)
{
final
ui
.
PictureRecorder
recorder
=
ui
.
PictureRecorder
();
final
Canvas
canvas
=
Canvas
(
recorder
);
canvas
.
drawPath
(
child
.
clipPath
,
Paint
()
..
color
=
const
Color
(
0xFFAA0000
)
..
style
=
PaintingStyle
.
stroke
// The elevation may be 0 or otherwise too small to notice.
// Adding 10 to it makes it more visually obvious.
..
strokeWidth
=
child
.
elevation
+
10.0
,
);
final
PictureLayer
pictureLayer
=
PictureLayer
(
child
.
clipPath
.
getBounds
())
..
picture
=
recorder
.
endRecording
()
..
debugCreator
=
child
;
child
.
append
(
pictureLayer
);
return
pictureLayer
;
}
List
<
PictureLayer
>
_processConflictingPhysicalLayers
(
PhysicalModelLayer
predecessor
,
PhysicalModelLayer
child
)
{
FlutterError
.
reportError
(
FlutterErrorDetails
(
exception:
FlutterError
(
'Painting order is out of order with respect to elevation.
\n
'
'See https://api.flutter.dev/flutter/rendering/debugCheckElevations.html '
'for more details.'
),
library
:
'rendering library'
,
context:
'during compositing'
,
informationCollector:
(
StringBuffer
buffer
)
{
buffer
.
writeln
(
'Attempted to composite layer:'
);
buffer
.
writeln
(
child
);
buffer
.
writeln
(
'after layer:'
);
buffer
.
writeln
(
predecessor
);
buffer
.
writeln
(
'which occupies the same area at a higher elevation.'
);
}
));
return
<
PictureLayer
>[
_highlightConflictingLayer
(
predecessor
),
_highlightConflictingLayer
(
child
),
];
}
/// Checks that no [PhysicalModelLayer] would paint after another overlapping
/// [PhysicalModelLayer] that has a higher elevation.
///
/// Returns a list of [PictureLayer] objects it added to the tree to highlight
/// bad nodes. These layers should be removed from the tree after building the
/// [Scene].
List
<
PictureLayer
>
_debugCheckElevations
()
{
final
List
<
PhysicalModelLayer
>
physicalModelLayers
=
depthFirstIterateChildren
().
whereType
<
PhysicalModelLayer
>().
toList
();
final
List
<
PictureLayer
>
addedLayers
=
<
PictureLayer
>[];
for
(
int
i
=
0
;
i
<
physicalModelLayers
.
length
;
i
++)
{
final
PhysicalModelLayer
physicalModelLayer
=
physicalModelLayers
[
i
];
assert
(
physicalModelLayer
.
lastChild
?.
debugCreator
!=
physicalModelLayer
,
'debugCheckElevations has either already visited this layer or failed '
'to remove the added picture from it.'
,
);
double
accumulatedElevation
=
physicalModelLayer
.
elevation
;
Layer
ancestor
=
physicalModelLayer
.
parent
;
while
(
ancestor
!=
null
)
{
if
(
ancestor
is
PhysicalModelLayer
)
{
accumulatedElevation
+=
ancestor
.
elevation
;
}
ancestor
=
ancestor
.
parent
;
}
for
(
int
j
=
0
;
j
<=
i
;
j
++)
{
final
PhysicalModelLayer
predecessor
=
physicalModelLayers
[
j
];
double
predecessorAccumulatedElevation
=
predecessor
.
elevation
;
ancestor
=
predecessor
.
parent
;
while
(
ancestor
!=
null
)
{
if
(
ancestor
==
predecessor
)
{
continue
;
}
if
(
ancestor
is
PhysicalModelLayer
)
{
predecessorAccumulatedElevation
+=
ancestor
.
elevation
;
}
ancestor
=
ancestor
.
parent
;
}
if
(
predecessorAccumulatedElevation
<=
accumulatedElevation
)
{
continue
;
}
final
Path
intersection
=
Path
.
combine
(
PathOperation
.
intersect
,
predecessor
.
_debugTransformedClipPath
,
physicalModelLayer
.
_debugTransformedClipPath
,
);
if
(
intersection
!=
null
&&
intersection
.
computeMetrics
().
any
((
ui
.
PathMetric
metric
)
=>
metric
.
length
>
0
))
{
addedLayers
.
addAll
(
_processConflictingPhysicalLayers
(
predecessor
,
physicalModelLayer
));
}
}
}
return
addedLayers
;
}
@override
void
updateSubtreeNeedsAddToScene
()
{
super
.
updateSubtreeNeedsAddToScene
();
...
...
@@ -679,6 +774,23 @@ class ContainerLayer extends Layer {
assert
(
transform
!=
null
);
}
/// Returns the descendants of this layer in depth first order.
@visibleForTesting
List
<
Layer
>
depthFirstIterateChildren
()
{
if
(
firstChild
==
null
)
return
<
Layer
>[];
final
List
<
Layer
>
children
=
<
Layer
>[];
Layer
child
=
firstChild
;
while
(
child
!=
null
)
{
children
.
add
(
child
);
if
(
child
is
ContainerLayer
)
{
children
.
addAll
(
child
.
depthFirstIterateChildren
());
}
child
=
child
.
nextSibling
;
}
return
children
;
}
@override
List
<
DiagnosticsNode
>
debugDescribeChildren
()
{
final
List
<
DiagnosticsNode
>
children
=
<
DiagnosticsNode
>[];
...
...
@@ -744,9 +856,29 @@ class OffsetLayer extends ContainerLayer {
/// Consider this layer as the root and build a scene (a tree of layers)
/// in the engine.
ui
.
Scene
buildScene
(
ui
.
SceneBuilder
builder
)
{
List
<
PictureLayer
>
temporaryLayers
;
assert
(()
{
if
(
debugCheckElevationsEnabled
)
{
temporaryLayers
=
_debugCheckElevations
();
}
return
true
;
}());
updateSubtreeNeedsAddToScene
();
addToScene
(
builder
);
return
builder
.
build
();
final
ui
.
Scene
scene
=
builder
.
build
();
assert
(()
{
// We should remove any layers that got added to highlight the incorrect
// PhysicalModelLayers. If we don't, we'll end up adding duplicate layers
// or potentially leaving a physical model that is now correct highlighted
// in red.
if
(
temporaryLayers
!=
null
)
{
for
(
PictureLayer
temporaryLayer
in
temporaryLayers
)
{
temporaryLayer
.
remove
();
}
}
return
true
;
}());
return
scene
;
}
@override
...
...
@@ -1090,7 +1222,12 @@ class TransformLayer extends OffsetLayer {
void
applyTransform
(
Layer
child
,
Matrix4
transform
)
{
assert
(
child
!=
null
);
assert
(
transform
!=
null
);
transform
.
multiply
(
_lastEffectiveTransform
);
assert
(
_lastEffectiveTransform
!=
null
||
this
.
transform
!=
null
);
if
(
_lastEffectiveTransform
==
null
)
{
transform
.
multiply
(
this
.
transform
);
}
else
{
transform
.
multiply
(
_lastEffectiveTransform
);
}
}
@override
...
...
@@ -1309,6 +1446,16 @@ class PhysicalModelLayer extends ContainerLayer {
}
}
Path
get
_debugTransformedClipPath
{
ContainerLayer
ancestor
=
parent
;
final
Matrix4
matrix
=
Matrix4
.
identity
();
while
(
ancestor
!=
null
&&
ancestor
.
parent
!=
null
)
{
ancestor
.
applyTransform
(
this
,
matrix
);
ancestor
=
ancestor
.
parent
;
}
return
clipPath
.
transform
(
matrix
.
storage
);
}
/// {@macro flutter.widgets.Clip}
Clip
get
clipBehavior
=>
_clipBehavior
;
Clip
_clipBehavior
;
...
...
packages/flutter/lib/src/rendering/proxy_box.dart
View file @
d2790bd2
...
...
@@ -1709,6 +1709,10 @@ class RenderPhysicalModel extends _RenderPhysicalModelBase<RRect> {
color:
color
,
shadowColor:
shadowColor
,
);
assert
(()
{
physicalModel
.
debugCreator
=
debugCreator
;
return
true
;
}());
context
.
pushLayer
(
physicalModel
,
super
.
paint
,
offset
,
childPaintBounds:
offsetBounds
);
}
}
...
...
@@ -1799,6 +1803,10 @@ class RenderPhysicalShape extends _RenderPhysicalModelBase<Path> {
color:
color
,
shadowColor:
shadowColor
,
);
assert
(()
{
physicalModel
.
debugCreator
=
debugCreator
;
return
true
;
}());
context
.
pushLayer
(
physicalModel
,
super
.
paint
,
offset
,
childPaintBounds:
offsetBounds
);
}
}
...
...
packages/flutter/test/foundation/service_extensions_test.dart
View file @
d2790bd2
...
...
@@ -117,7 +117,7 @@ Future<Map<String, dynamic>> hasReassemble(Future<Map<String, dynamic>> pendingR
void
main
(
)
{
final
List
<
String
>
console
=
<
String
>[];
test
(
'Service extensions - pretest'
,
()
async
{
setUpAll
(
()
async
{
binding
=
TestServiceExtensionsBinding
();
expect
(
binding
.
frameScheduled
,
isTrue
);
...
...
@@ -142,10 +142,25 @@ void main() {
};
});
tearDownAll
(()
async
{
// See widget_inspector_test.dart for tests of the ext.flutter.inspector
// service extensions included in this count.
int
widgetInspectorExtensionCount
=
15
;
if
(
WidgetInspectorService
.
instance
.
isWidgetCreationTracked
())
{
// Some inspector extensions are only exposed if widget creation locations
// are tracked.
widgetInspectorExtensionCount
+=
2
;
}
// If you add a service extension... TEST IT! :-)
// ...then increment this number.
expect
(
binding
.
extensions
.
length
,
26
+
widgetInspectorExtensionCount
);
expect
(
console
,
isEmpty
);
debugPrint
=
debugPrintThrottled
;
});
// The following list is alphabetical, one test per extension.
//
// The order doesn't really matter except that the pretest and posttest tests
// must be first and last respectively.
test
(
'Service extensions - debugAllowBanner'
,
()
async
{
Map
<
String
,
dynamic
>
result
;
...
...
@@ -170,6 +185,34 @@ void main() {
expect
(
binding
.
frameScheduled
,
isFalse
);
});
test
(
'Service extensions - debugCheckElevationsEnabled'
,
()
async
{
expect
(
binding
.
frameScheduled
,
isFalse
);
expect
(
debugCheckElevationsEnabled
,
false
);
bool
lastValue
=
false
;
Future
<
void
>
_updateAndCheck
(
bool
newValue
)
async
{
Map
<
String
,
dynamic
>
result
;
binding
.
testExtension
(
'debugCheckElevationsEnabled'
,
<
String
,
String
>{
'enabled'
:
'
$newValue
'
}
).
then
((
Map
<
String
,
dynamic
>
answer
)
=>
result
=
answer
);
await
binding
.
flushMicrotasks
();
expect
(
binding
.
frameScheduled
,
lastValue
!=
newValue
);
await
binding
.
doFrame
();
await
binding
.
flushMicrotasks
();
expect
(
result
,
<
String
,
String
>{
'enabled'
:
'
$newValue
'
});
expect
(
debugCheckElevationsEnabled
,
newValue
);
lastValue
=
newValue
;
}
await
_updateAndCheck
(
false
);
await
_updateAndCheck
(
true
);
await
_updateAndCheck
(
true
);
await
_updateAndCheck
(
false
);
await
_updateAndCheck
(
false
);
expect
(
binding
.
frameScheduled
,
isFalse
);
});
test
(
'Service extensions - debugDumpApp'
,
()
async
{
Map
<
String
,
dynamic
>
result
;
...
...
@@ -617,22 +660,4 @@ void main() {
expect
(
trace
,
contains
(
'package:test_api/test_api.dart,::,test
\n
'
));
expect
(
trace
,
contains
(
'service_extensions_test.dart,::,main
\n
'
));
});
test
(
'Service extensions - posttest'
,
()
async
{
// See widget_inspector_test.dart for tests of the ext.flutter.inspector
// service extensions included in this count.
int
widgetInspectorExtensionCount
=
15
;
if
(
WidgetInspectorService
.
instance
.
isWidgetCreationTracked
())
{
// Some inspector extensions are only exposed if widget creation locations
// are tracked.
widgetInspectorExtensionCount
+=
2
;
}
// If you add a service extension... TEST IT! :-)
// ...then increment this number.
expect
(
binding
.
extensions
.
length
,
25
+
widgetInspectorExtensionCount
);
expect
(
console
,
isEmpty
);
debugPrint
=
debugPrintThrottled
;
});
}
packages/flutter/test/rendering/layers_test.dart
View file @
d2790bd2
...
...
@@ -4,6 +4,7 @@
import
'dart:ui'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
...
@@ -112,6 +113,56 @@ void main() {
expect
(
followerLayer
.
debugSubtreeNeedsAddToScene
,
true
);
});
test
(
'depthFirstIterateChildren'
,
()
{
final
ContainerLayer
a
=
ContainerLayer
();
final
ContainerLayer
b
=
ContainerLayer
();
final
ContainerLayer
c
=
ContainerLayer
();
final
ContainerLayer
d
=
ContainerLayer
();
final
ContainerLayer
e
=
ContainerLayer
();
final
ContainerLayer
f
=
ContainerLayer
();
final
ContainerLayer
g
=
ContainerLayer
();
final
PictureLayer
h
=
PictureLayer
(
Rect
.
zero
);
final
PictureLayer
i
=
PictureLayer
(
Rect
.
zero
);
final
PictureLayer
j
=
PictureLayer
(
Rect
.
zero
);
// The tree is like the following:
// a____
// / \
// b___ c
// / \ \ |
// d e f g
// / \ |
// h i j
a
.
append
(
b
);
a
.
append
(
c
);
b
.
append
(
d
);
b
.
append
(
e
);
b
.
append
(
f
);
d
.
append
(
h
);
d
.
append
(
i
);
c
.
append
(
g
);
g
.
append
(
j
);
expect
(
a
.
depthFirstIterateChildren
(),
<
Layer
>[
b
,
d
,
h
,
i
,
e
,
f
,
c
,
g
,
j
],
);
d
.
remove
();
// a____
// / \
// b___ c
// \ \ |
// e f g
// |
// j
expect
(
a
.
depthFirstIterateChildren
(),
<
Layer
>[
b
,
e
,
f
,
c
,
g
,
j
],
);
});
void
checkNeedsAddToScene
(
Layer
layer
,
void
mutateCallback
())
{
layer
.
debugMarkClean
();
layer
.
updateSubtreeNeedsAddToScene
();
...
...
@@ -239,4 +290,170 @@ void main() {
layer
.
shadowColor
=
const
Color
(
1
);
});
});
group
(
'PhysicalModelLayer checks elevations'
,
()
{
/// Adds the layers to a container where A paints before B.
///
/// Expects there to be `expectedErrorCount` errors. Checking elevations is
/// enabled by default.
void
_testConflicts
(
PhysicalModelLayer
layerA
,
PhysicalModelLayer
layerB
,
{
@required
int
expectedErrorCount
,
bool
enableCheck
=
true
,
})
{
assert
(
expectedErrorCount
!=
null
);
assert
(
enableCheck
||
expectedErrorCount
==
0
,
'Cannot disable check and expect non-zero error count.'
);
final
OffsetLayer
container
=
OffsetLayer
();
container
.
append
(
layerA
);
container
.
append
(
layerB
);
debugCheckElevationsEnabled
=
enableCheck
;
debugDisableShadows
=
false
;
int
errors
=
0
;
if
(
enableCheck
)
{
FlutterError
.
onError
=
(
FlutterErrorDetails
details
)
{
errors
++;
};
}
container
.
buildScene
(
SceneBuilder
());
expect
(
errors
,
expectedErrorCount
);
debugCheckElevationsEnabled
=
false
;
}
// Tests:
//
// ───────────── (LayerA, paints first)
// │ ───────────── (LayerB, paints second)
// │ │
// ───────────────────────────
test
(
'Overlapping layers at wrong elevation'
,
()
{
final
PhysicalModelLayer
layerA
=
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
0
,
0
,
20
,
20
)),
elevation:
3.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
);
final
PhysicalModelLayer
layerB
=
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
10
,
10
,
20
,
20
)),
elevation:
2.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
);
_testConflicts
(
layerA
,
layerB
,
expectedErrorCount:
1
);
});
// Tests:
//
// ───────────── (LayerA, paints first)
// │ ───────────── (LayerB, paints second)
// │ │
// ───────────────────────────
//
// Causes no error if check is disabled.
test
(
'Overlapping layers at wrong elevation, check disabled'
,
()
{
final
PhysicalModelLayer
layerA
=
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
0
,
0
,
20
,
20
)),
elevation:
3.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
);
final
PhysicalModelLayer
layerB
=
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
10
,
10
,
20
,
20
)),
elevation:
2.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
);
_testConflicts
(
layerA
,
layerB
,
expectedErrorCount:
0
,
enableCheck:
false
);
});
// Tests:
//
// ────────── (LayerA, paints first)
// │ ─────────── (LayerB, paints second)
// │ │
// ────────────────────────────
test
(
'Non-overlapping layers at wrong elevation'
,
()
{
final
PhysicalModelLayer
layerA
=
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
0
,
0
,
20
,
20
)),
elevation:
3.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
);
final
PhysicalModelLayer
layerB
=
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
20
,
20
,
20
,
20
)),
elevation:
2.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
);
_testConflicts
(
layerA
,
layerB
,
expectedErrorCount:
0
);
});
// Tests:
//
// ─────── (Child of A, paints second)
// │
// ─────────── (LayerA, paints first)
// │ ──────────── (LayerB, paints third)
// │ │
// ────────────────────────────
test
(
'Non-overlapping layers at wrong elevation, child at lower elevation'
,
()
{
final
PhysicalModelLayer
layerA
=
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
0
,
0
,
20
,
20
)),
elevation:
3.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
);
layerA
.
append
(
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
2
,
2
,
10
,
10
)),
elevation:
1.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
));
final
PhysicalModelLayer
layerB
=
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
20
,
20
,
20
,
20
)),
elevation:
2.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
);
_testConflicts
(
layerA
,
layerB
,
expectedErrorCount:
0
);
});
// Tests:
//
// ─────────── (Child of A, paints second, overflows)
// │ ──────────── (LayerB, paints third)
// ─────────── │ (LayerA, paints first)
// │ │
// │ │
// ────────────────────────────
//
// Which fails because the overflowing child overlaps something that paints
// after it at a lower elevation.
test
(
'Child overflows parent and overlaps another physical layer'
,
()
{
final
PhysicalModelLayer
layerA
=
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
0
,
0
,
20
,
20
)),
elevation:
3.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
);
layerA
.
append
(
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
15
,
15
,
25
,
25
)),
elevation:
2.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
));
final
PhysicalModelLayer
layerB
=
PhysicalModelLayer
(
clipPath:
Path
()..
addRect
(
Rect
.
fromLTWH
(
20
,
20
,
20
,
20
)),
elevation:
4.0
,
color:
const
Color
(
0
),
shadowColor:
const
Color
(
0
),
);
_testConflicts
(
layerA
,
layerB
,
expectedErrorCount:
1
);
});
});
}
packages/flutter/test/widgets/physical_model_test.dart
View file @
d2790bd2
...
...
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import
'dart:io'
show
Platform
;
import
'dart:math'
as
math
show
pi
;
import
'package:flutter/material.dart'
;
import
'package:flutter/rendering.dart'
;
...
...
@@ -69,4 +70,418 @@ void main() {
skip:
!
Platform
.
isLinux
,
);
});
group
(
'PhysicalModelLayer checks elevation'
,
()
{
Future
<
void
>
_testStackChildren
(
WidgetTester
tester
,
List
<
Widget
>
children
,
{
@required
int
expectedErrorCount
,
bool
enableCheck
=
true
,
})
async
{
assert
(
expectedErrorCount
!=
null
);
if
(
enableCheck
)
{
debugCheckElevationsEnabled
=
true
;
}
else
{
assert
(
expectedErrorCount
==
0
,
'Cannot expect errors if check is disabled.'
);
}
debugDisableShadows
=
false
;
int
count
=
0
;
final
Function
oldOnError
=
FlutterError
.
onError
;
FlutterError
.
onError
=
(
FlutterErrorDetails
details
)
{
count
++;
};
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Stack
(
children:
children
,
),
),
);
FlutterError
.
onError
=
oldOnError
;
expect
(
count
,
expectedErrorCount
);
if
(
enableCheck
)
{
debugCheckElevationsEnabled
=
false
;
}
debugDisableShadows
=
true
;
}
// Tests:
//
// ─────────── (red rect, paints second, child)
// │
// ─────────── (green rect, paints first)
// │
// ────────────────────────────
testWidgets
(
'entirely overlapping, direct child'
,
(
WidgetTester
tester
)
async
{
final
List
<
Widget
>
children
=
<
Widget
>[
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
1.0
,
color:
Colors
.
green
,
child:
Material
(
elevation:
2.0
,
color:
Colors
.
red
,
)
),
),
];
await
_testStackChildren
(
tester
,
children
,
expectedErrorCount:
0
);
expect
(
find
.
byType
(
Material
),
findsNWidgets
(
2
));
});
// Tests:
//
// ─────────────── (green rect, paints second)
// ─────────── │ (blue rect, paints first)
// │ │
// │ │
// ────────────────────────────
testWidgets
(
'entirely overlapping, correct painting order'
,
(
WidgetTester
tester
)
async
{
final
List
<
Widget
>
children
=
<
Widget
>[
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
1.0
,
color:
Colors
.
green
,
),
),
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
2.0
,
color:
Colors
.
blue
,
),
),
];
await
_testStackChildren
(
tester
,
children
,
expectedErrorCount:
0
);
expect
(
find
.
byType
(
Material
),
findsNWidgets
(
2
));
});
// Tests:
//
// ─────────────── (green rect, paints first)
// │ ─────────── (blue rect, paints second)
// │ │
// │ │
// ────────────────────────────
testWidgets
(
'entirely overlapping, wrong painting order'
,
(
WidgetTester
tester
)
async
{
final
List
<
Widget
>
children
=
<
Widget
>[
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
2.0
,
color:
Colors
.
green
,
),
),
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
1.0
,
color:
Colors
.
blue
,
),
),
];
await
_testStackChildren
(
tester
,
children
,
expectedErrorCount:
1
);
expect
(
find
.
byType
(
Material
),
findsNWidgets
(
2
));
});
// Tests:
//
// ─────────────── (brown rect, paints first)
// │ ─────────── (red circle, paints second)
// │ │
// │ │
// ────────────────────────────
testWidgets
(
'not non-rect not overlapping, wrong painting order'
,
(
WidgetTester
tester
)
async
{
// These would be overlapping if we only took the rectangular bounds of the circle.
final
List
<
Widget
>
children
=
<
Widget
>[
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
150
,
150
,
150
,
150
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
3.0
,
color:
Colors
.
brown
,
),
),
),
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
20
,
20
,
140
,
150
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
2.0
,
color:
Colors
.
red
,
shape:
CircleBorder
()
),
),
),
];
await
_testStackChildren
(
tester
,
children
,
expectedErrorCount:
0
);
expect
(
find
.
byType
(
Material
),
findsNWidgets
(
2
));
});
// Tests:
//
// ─────────────── (brown rect, paints first)
// │ ─────────── (red circle, paints second)
// │ │
// │ │
// ────────────────────────────
testWidgets
(
'not non-rect entirely overlapping, wrong painting order'
,
(
WidgetTester
tester
)
async
{
final
List
<
Widget
>
children
=
<
Widget
>[
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
20
,
20
,
140
,
150
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
3.0
,
color:
Colors
.
brown
,
),
),
),
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
50
,
50
,
100
,
100
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
2.0
,
color:
Colors
.
red
,
shape:
CircleBorder
()
),
),
),
];
await
_testStackChildren
(
tester
,
children
,
expectedErrorCount:
1
);
expect
(
find
.
byType
(
Material
),
findsNWidgets
(
2
));
});
// Tests:
//
// ─────────────── (brown rect, paints first)
// │ ──────────── (red circle, paints second)
// │ │
// │ │
// ────────────────────────────
testWidgets
(
'non-rect partially overlapping, wrong painting order'
,
(
WidgetTester
tester
)
async
{
final
List
<
Widget
>
children
=
<
Widget
>[
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
150
,
150
,
150
,
150
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
3.0
,
color:
Colors
.
brown
,
),
),
),
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
30
,
20
,
150
,
150
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
2.0
,
color:
Colors
.
red
,
shape:
CircleBorder
()
),
),
),
];
await
_testStackChildren
(
tester
,
children
,
expectedErrorCount:
1
);
expect
(
find
.
byType
(
Material
),
findsNWidgets
(
2
));
});
// Tests:
//
// ─────────────── (green rect, paints second, overlaps red rect)
// │
// │
// ────────────────────────── (brown and red rects, overlapping but same elevation, paint first and third)
// │ │
// ────────────────────────────
//
// Fails because the green rect overlaps the
testWidgets
(
'child partially overlapping, wrong painting order'
,
(
WidgetTester
tester
)
async
{
final
List
<
Widget
>
children
=
<
Widget
>[
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
150
,
150
,
150
,
150
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
1.0
,
color:
Colors
.
brown
,
child:
Padding
(
padding:
EdgeInsets
.
all
(
30.0
),
child:
Material
(
elevation:
2.0
,
color:
Colors
.
green
,
),
),
),
),
),
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
30
,
20
,
180
,
180
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
1.0
,
color:
Colors
.
red
,
),
),
),
];
await
_testStackChildren
(
tester
,
children
,
expectedErrorCount:
1
);
expect
(
find
.
byType
(
Material
),
findsNWidgets
(
3
));
});
// Tests:
//
// ─────────────── (brown rect, paints first)
// │ ──────────── (red circle, paints second)
// │ │
// │ │
// ────────────────────────────
testWidgets
(
'non-rect partially overlapping, wrong painting order, check disabled'
,
(
WidgetTester
tester
)
async
{
final
List
<
Widget
>
children
=
<
Widget
>[
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
150
,
150
,
150
,
150
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
3.0
,
color:
Colors
.
brown
,
),
),
),
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
30
,
20
,
150
,
150
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
2.0
,
color:
Colors
.
red
,
shape:
CircleBorder
()
),
),
),
];
await
_testStackChildren
(
tester
,
children
,
expectedErrorCount:
0
,
enableCheck:
false
,
);
expect
(
find
.
byType
(
Material
),
findsNWidgets
(
2
));
});
// Tests:
//
// ──────────── (brown rect, paints first, rotated but doesn't overlap)
// │ ──────────── (red circle, paints second)
// │ │
// │ │
// ────────────────────────────
testWidgets
(
'with a RenderTransform, non-overlapping'
,
(
WidgetTester
tester
)
async
{
final
List
<
Widget
>
children
=
<
Widget
>[
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
140
,
100
,
140
,
150
),
child:
Container
(
width:
300
,
height:
300
,
child:
Transform
.
rotate
(
angle:
math
.
pi
/
180
*
15
,
child:
const
Material
(
elevation:
3.0
,
color:
Colors
.
brown
,
),
),
),
),
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
50
,
50
,
100
,
100
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
2.0
,
color:
Colors
.
red
,
shape:
CircleBorder
()),
),
),
];
await
_testStackChildren
(
tester
,
children
,
expectedErrorCount:
0
);
expect
(
find
.
byType
(
Material
),
findsNWidgets
(
2
));
});
// Tests:
//
// ────────────── (brown rect, paints first, rotated so it overlaps)
// │ ──────────── (red circle, paints second)
// │ │
// │ │
// ────────────────────────────
// This would be fine without the rotation.
testWidgets
(
'with a RenderTransform, overlapping'
,
(
WidgetTester
tester
)
async
{
final
List
<
Widget
>
children
=
<
Widget
>[
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
140
,
100
,
140
,
150
),
child:
Container
(
width:
300
,
height:
300
,
child:
Transform
.
rotate
(
angle:
math
.
pi
/
180
*
8
,
child:
const
Material
(
elevation:
3.0
,
color:
Colors
.
brown
,
),
),
),
),
Positioned
.
fromRect
(
rect:
Rect
.
fromLTWH
(
50
,
50
,
100
,
100
),
child:
Container
(
width:
300
,
height:
300
,
child:
const
Material
(
elevation:
2.0
,
color:
Colors
.
red
,
shape:
CircleBorder
()),
),
),
];
await
_testStackChildren
(
tester
,
children
,
expectedErrorCount:
1
);
expect
(
find
.
byType
(
Material
),
findsNWidgets
(
2
));
});
});
}
packages/flutter_tools/lib/src/resident_runner.dart
View file @
d2790bd2
...
...
@@ -272,6 +272,11 @@ class FlutterDevice {
await
view
.
uiIsolate
.
flutterToggleDebugPaintSizeEnabled
();
}
Future
<
void
>
toggleDebugCheckElevationsEnabled
()
async
{
for
(
FlutterView
view
in
views
)
await
view
.
uiIsolate
.
flutterToggleDebugCheckElevationsEnabled
();
}
Future
<
void
>
debugTogglePerformanceOverlayOverride
()
async
{
for
(
FlutterView
view
in
views
)
await
view
.
uiIsolate
.
flutterTogglePerformanceOverlayOverride
();
...
...
@@ -620,6 +625,12 @@ abstract class ResidentRunner {
await
device
.
toggleDebugPaintSizeEnabled
();
}
Future
<
void
>
_debugToggleDebugCheckElevationsEnabled
()
async
{
await
refreshViews
();
for
(
FlutterDevice
device
in
flutterDevices
)
await
device
.
toggleDebugCheckElevationsEnabled
();
}
Future
<
void
>
_debugTogglePerformanceOverlayOverride
()
async
{
await
refreshViews
();
for
(
FlutterDevice
device
in
flutterDevices
)
...
...
@@ -871,6 +882,9 @@ abstract class ResidentRunner {
}
else
if
(
lower
==
'd'
)
{
await
detach
();
return
true
;
}
else
if
(
lower
==
'z'
)
{
await
_debugToggleDebugCheckElevationsEnabled
();
return
true
;
}
return
false
;
...
...
@@ -962,6 +976,7 @@ abstract class ResidentRunner {
printStatus
(
'To toggle the widget inspector (WidgetsApp.showWidgetInspectorOverride), press "i".'
);
printStatus
(
'To toggle the display of construction lines (debugPaintSizeEnabled), press "p".'
);
printStatus
(
'To simulate different operating systems, (defaultTargetPlatform), press "o".'
);
printStatus
(
'To toggle the elevation checker, press "z".'
);
}
else
{
printStatus
(
'To dump the accessibility tree (debugDumpSemantics), press "S" (for traversal order) or "U" (for inverse hit test order).'
);
}
...
...
packages/flutter_tools/lib/src/vmservice.dart
View file @
d2790bd2
...
...
@@ -1283,6 +1283,8 @@ class Isolate extends ServiceObjectOwner {
Future
<
Map
<
String
,
dynamic
>>
flutterToggleDebugPaintSizeEnabled
()
=>
_flutterToggle
(
'debugPaint'
);
Future
<
Map
<
String
,
dynamic
>>
flutterToggleDebugCheckElevationsEnabled
()
=>
_flutterToggle
(
'debugCheckElevationsEnabled'
);
Future
<
Map
<
String
,
dynamic
>>
flutterTogglePerformanceOverlayOverride
()
=>
_flutterToggle
(
'showPerformanceOverlay'
);
Future
<
Map
<
String
,
dynamic
>>
flutterToggleWidgetInspector
()
=>
_flutterToggle
(
'inspector.show'
);
...
...
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