mirror of
https://github.com/flutter/flutter.git
synced 2026-02-11 13:22:46 +08:00
1097 lines
36 KiB
Dart
1097 lines
36 KiB
Dart
// 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 'dart:ui';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'rendering_tester.dart';
|
|
|
|
void main() {
|
|
TestRenderingFlutterBinding.ensureInitialized();
|
|
|
|
test('non-painted layers are detached', () {
|
|
RenderObject boundary, inner;
|
|
final RenderOpacity root = RenderOpacity(
|
|
child: boundary = RenderRepaintBoundary(
|
|
child: inner = RenderDecoratedBox(
|
|
decoration: const BoxDecoration(),
|
|
),
|
|
),
|
|
);
|
|
layout(root, phase: EnginePhase.paint);
|
|
expect(inner.isRepaintBoundary, isFalse);
|
|
expect(inner.debugLayer, null);
|
|
expect(boundary.isRepaintBoundary, isTrue);
|
|
expect(boundary.debugLayer, isNotNull);
|
|
expect(boundary.debugLayer!.attached, isTrue); // this time it painted...
|
|
|
|
root.opacity = 0.0;
|
|
pumpFrame(phase: EnginePhase.paint);
|
|
expect(inner.isRepaintBoundary, isFalse);
|
|
expect(inner.debugLayer, null);
|
|
expect(boundary.isRepaintBoundary, isTrue);
|
|
expect(boundary.debugLayer, isNotNull);
|
|
expect(boundary.debugLayer!.attached, isFalse); // this time it did not.
|
|
|
|
root.opacity = 0.5;
|
|
pumpFrame(phase: EnginePhase.paint);
|
|
expect(inner.isRepaintBoundary, isFalse);
|
|
expect(inner.debugLayer, null);
|
|
expect(boundary.isRepaintBoundary, isTrue);
|
|
expect(boundary.debugLayer, isNotNull);
|
|
expect(boundary.debugLayer!.attached, isTrue); // this time it did again!
|
|
});
|
|
|
|
test('updateSubtreeNeedsAddToScene propagates Layer.alwaysNeedsAddToScene up the tree', () {
|
|
final ContainerLayer a = ContainerLayer();
|
|
final ContainerLayer b = ContainerLayer();
|
|
final ContainerLayer c = ContainerLayer();
|
|
final _TestAlwaysNeedsAddToSceneLayer d = _TestAlwaysNeedsAddToSceneLayer();
|
|
final ContainerLayer e = ContainerLayer();
|
|
final ContainerLayer f = ContainerLayer();
|
|
|
|
// Tree structure:
|
|
// a
|
|
// / \
|
|
// b c
|
|
// / \
|
|
// (x)d e
|
|
// /
|
|
// f
|
|
a.append(b);
|
|
a.append(c);
|
|
b.append(d);
|
|
b.append(e);
|
|
d.append(f);
|
|
|
|
a.debugMarkClean();
|
|
b.debugMarkClean();
|
|
c.debugMarkClean();
|
|
d.debugMarkClean();
|
|
e.debugMarkClean();
|
|
f.debugMarkClean();
|
|
|
|
expect(a.debugSubtreeNeedsAddToScene, false);
|
|
expect(b.debugSubtreeNeedsAddToScene, false);
|
|
expect(c.debugSubtreeNeedsAddToScene, false);
|
|
expect(d.debugSubtreeNeedsAddToScene, false);
|
|
expect(e.debugSubtreeNeedsAddToScene, false);
|
|
expect(f.debugSubtreeNeedsAddToScene, false);
|
|
|
|
a.updateSubtreeNeedsAddToScene();
|
|
|
|
expect(a.debugSubtreeNeedsAddToScene, true);
|
|
expect(b.debugSubtreeNeedsAddToScene, true);
|
|
expect(c.debugSubtreeNeedsAddToScene, false);
|
|
expect(d.debugSubtreeNeedsAddToScene, true);
|
|
expect(e.debugSubtreeNeedsAddToScene, false);
|
|
expect(f.debugSubtreeNeedsAddToScene, false);
|
|
});
|
|
|
|
test('updateSubtreeNeedsAddToScene propagates Layer._needsAddToScene up the tree', () {
|
|
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 List<ContainerLayer> allLayers = <ContainerLayer>[a, b, c, d, e, f, g];
|
|
|
|
// The tree is like the following where b and j are dirty:
|
|
// a____
|
|
// / \
|
|
// (x)b___ c
|
|
// / \ \ |
|
|
// d e f g(x)
|
|
a.append(b);
|
|
a.append(c);
|
|
b.append(d);
|
|
b.append(e);
|
|
b.append(f);
|
|
c.append(g);
|
|
|
|
for (final ContainerLayer layer in allLayers) {
|
|
expect(layer.debugSubtreeNeedsAddToScene, true);
|
|
}
|
|
|
|
for (final ContainerLayer layer in allLayers) {
|
|
layer.debugMarkClean();
|
|
}
|
|
|
|
for (final ContainerLayer layer in allLayers) {
|
|
expect(layer.debugSubtreeNeedsAddToScene, false);
|
|
}
|
|
|
|
b.markNeedsAddToScene();
|
|
a.updateSubtreeNeedsAddToScene();
|
|
|
|
expect(a.debugSubtreeNeedsAddToScene, true);
|
|
expect(b.debugSubtreeNeedsAddToScene, true);
|
|
expect(c.debugSubtreeNeedsAddToScene, false);
|
|
expect(d.debugSubtreeNeedsAddToScene, false);
|
|
expect(e.debugSubtreeNeedsAddToScene, false);
|
|
expect(f.debugSubtreeNeedsAddToScene, false);
|
|
expect(g.debugSubtreeNeedsAddToScene, false);
|
|
|
|
g.markNeedsAddToScene();
|
|
a.updateSubtreeNeedsAddToScene();
|
|
|
|
expect(a.debugSubtreeNeedsAddToScene, true);
|
|
expect(b.debugSubtreeNeedsAddToScene, true);
|
|
expect(c.debugSubtreeNeedsAddToScene, true);
|
|
expect(d.debugSubtreeNeedsAddToScene, false);
|
|
expect(e.debugSubtreeNeedsAddToScene, false);
|
|
expect(f.debugSubtreeNeedsAddToScene, false);
|
|
expect(g.debugSubtreeNeedsAddToScene, true);
|
|
|
|
a.buildScene(SceneBuilder());
|
|
for (final ContainerLayer layer in allLayers) {
|
|
expect(layer.debugSubtreeNeedsAddToScene, false);
|
|
}
|
|
});
|
|
|
|
test('follower layers are always dirty', () {
|
|
final LayerLink link = LayerLink();
|
|
final LeaderLayer leaderLayer = LeaderLayer(link: link);
|
|
final FollowerLayer followerLayer = FollowerLayer(link: link);
|
|
leaderLayer.debugMarkClean();
|
|
followerLayer.debugMarkClean();
|
|
leaderLayer.updateSubtreeNeedsAddToScene();
|
|
followerLayer.updateSubtreeNeedsAddToScene();
|
|
expect(followerLayer.debugSubtreeNeedsAddToScene, true);
|
|
});
|
|
|
|
test('switching layer link of an attached leader layer should not crash', () {
|
|
final LayerLink link = LayerLink();
|
|
final LeaderLayer leaderLayer = LeaderLayer(link: link);
|
|
final RenderView view = RenderView(configuration: const ViewConfiguration(), window: RendererBinding.instance.window);
|
|
leaderLayer.attach(view);
|
|
final LayerLink link2 = LayerLink();
|
|
leaderLayer.link = link2;
|
|
// This should not crash.
|
|
leaderLayer.detach();
|
|
expect(leaderLayer.link, link2);
|
|
});
|
|
|
|
test('layer link attach/detach order should not crash app.', () {
|
|
final LayerLink link = LayerLink();
|
|
final LeaderLayer leaderLayer1 = LeaderLayer(link: link);
|
|
final LeaderLayer leaderLayer2 = LeaderLayer(link: link);
|
|
final RenderView view = RenderView(configuration: const ViewConfiguration(), window: RendererBinding.instance.window);
|
|
leaderLayer1.attach(view);
|
|
leaderLayer2.attach(view);
|
|
leaderLayer2.detach();
|
|
leaderLayer1.detach();
|
|
expect(link.leader, isNull);
|
|
});
|
|
|
|
test('leader layers not dirty when connected to follower layer', () {
|
|
final ContainerLayer root = ContainerLayer()..attach(Object());
|
|
|
|
final LayerLink link = LayerLink();
|
|
final LeaderLayer leaderLayer = LeaderLayer(link: link);
|
|
final FollowerLayer followerLayer = FollowerLayer(link: link);
|
|
|
|
root.append(leaderLayer);
|
|
root.append(followerLayer);
|
|
|
|
leaderLayer.debugMarkClean();
|
|
followerLayer.debugMarkClean();
|
|
leaderLayer.updateSubtreeNeedsAddToScene();
|
|
followerLayer.updateSubtreeNeedsAddToScene();
|
|
expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
|
|
});
|
|
|
|
test('leader layers are not dirty when all followers disconnects', () {
|
|
final ContainerLayer root = ContainerLayer()..attach(Object());
|
|
final LayerLink link = LayerLink();
|
|
final LeaderLayer leaderLayer = LeaderLayer(link: link);
|
|
root.append(leaderLayer);
|
|
|
|
// Does not need add to scene when nothing is connected to link.
|
|
leaderLayer.debugMarkClean();
|
|
leaderLayer.updateSubtreeNeedsAddToScene();
|
|
expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
|
|
|
|
// Connecting a follower does not require adding to scene
|
|
final FollowerLayer follower1 = FollowerLayer(link: link);
|
|
root.append(follower1);
|
|
leaderLayer.debugMarkClean();
|
|
leaderLayer.updateSubtreeNeedsAddToScene();
|
|
expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
|
|
|
|
final FollowerLayer follower2 = FollowerLayer(link: link);
|
|
root.append(follower2);
|
|
leaderLayer.debugMarkClean();
|
|
leaderLayer.updateSubtreeNeedsAddToScene();
|
|
expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
|
|
|
|
// Disconnecting one follower, still does not needs add to scene.
|
|
follower2.remove();
|
|
leaderLayer.debugMarkClean();
|
|
leaderLayer.updateSubtreeNeedsAddToScene();
|
|
expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
|
|
|
|
// Disconnecting all followers goes back to not requiring add to scene.
|
|
follower1.remove();
|
|
leaderLayer.debugMarkClean();
|
|
leaderLayer.updateSubtreeNeedsAddToScene();
|
|
expect(leaderLayer.debugSubtreeNeedsAddToScene, false);
|
|
});
|
|
|
|
test('LeaderLayer.applyTransform can be called after retained rendering', () {
|
|
void expectTransform(RenderObject leader) {
|
|
final LeaderLayer leaderLayer = leader.debugLayer! as LeaderLayer;
|
|
final Matrix4 expected = Matrix4.identity()
|
|
..translate(leaderLayer.offset.dx, leaderLayer.offset.dy);
|
|
final Matrix4 transformed = Matrix4.identity();
|
|
leaderLayer.applyTransform(null, transformed);
|
|
expect(transformed, expected);
|
|
}
|
|
|
|
final LayerLink link = LayerLink();
|
|
late RenderLeaderLayer leader;
|
|
final RenderRepaintBoundary root = RenderRepaintBoundary(
|
|
child:RenderRepaintBoundary(
|
|
child: leader = RenderLeaderLayer(link: link),
|
|
),
|
|
);
|
|
layout(root, phase: EnginePhase.composite);
|
|
|
|
expectTransform(leader);
|
|
|
|
// Causes a repaint, but the LeaderLayer of RenderLeaderLayer will be added
|
|
// as retained and LeaderLayer.addChildrenToScene will not be called.
|
|
root.markNeedsPaint();
|
|
pumpFrame(phase: EnginePhase.composite);
|
|
|
|
// The LeaderLayer.applyTransform call shouldn't crash.
|
|
expectTransform(leader);
|
|
});
|
|
|
|
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 Function() mutateCallback) {
|
|
layer.debugMarkClean();
|
|
layer.updateSubtreeNeedsAddToScene();
|
|
expect(layer.debugSubtreeNeedsAddToScene, false);
|
|
mutateCallback();
|
|
layer.updateSubtreeNeedsAddToScene();
|
|
expect(layer.debugSubtreeNeedsAddToScene, true);
|
|
}
|
|
|
|
List<String> getDebugInfo(Layer layer) {
|
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
layer.debugFillProperties(builder);
|
|
return builder.properties
|
|
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
|
|
.map((DiagnosticsNode node) => node.toString()).toList();
|
|
}
|
|
|
|
test('ClipRectLayer prints clipBehavior in debug info', () {
|
|
expect(getDebugInfo(ClipRectLayer()), contains('clipBehavior: Clip.hardEdge'));
|
|
expect(
|
|
getDebugInfo(ClipRectLayer(clipBehavior: Clip.antiAliasWithSaveLayer)),
|
|
contains('clipBehavior: Clip.antiAliasWithSaveLayer'),
|
|
);
|
|
});
|
|
|
|
test('ClipRRectLayer prints clipBehavior in debug info', () {
|
|
expect(getDebugInfo(ClipRRectLayer()), contains('clipBehavior: Clip.antiAlias'));
|
|
expect(
|
|
getDebugInfo(ClipRRectLayer(clipBehavior: Clip.antiAliasWithSaveLayer)),
|
|
contains('clipBehavior: Clip.antiAliasWithSaveLayer'),
|
|
);
|
|
});
|
|
|
|
test('ClipPathLayer prints clipBehavior in debug info', () {
|
|
expect(getDebugInfo(ClipPathLayer()), contains('clipBehavior: Clip.antiAlias'));
|
|
expect(
|
|
getDebugInfo(ClipPathLayer(clipBehavior: Clip.antiAliasWithSaveLayer)),
|
|
contains('clipBehavior: Clip.antiAliasWithSaveLayer'),
|
|
);
|
|
});
|
|
|
|
test('BackdropFilterLayer prints filter and blendMode in debug info', () {
|
|
final ImageFilter filter = ImageFilter.blur(sigmaX: 1.0, sigmaY: 1.0, tileMode: TileMode.repeated);
|
|
final BackdropFilterLayer layer = BackdropFilterLayer(filter: filter, blendMode: BlendMode.clear);
|
|
final List<String> info = getDebugInfo(layer);
|
|
expect(info, contains(isBrowser ? 'filter: ImageFilter.blur(1, 1, TileMode.repeated)' : 'filter: ImageFilter.blur(1.0, 1.0, repeated)'));
|
|
expect(info, contains('blendMode: clear'));
|
|
});
|
|
|
|
test('PictureLayer prints picture, raster cache hints in debug info', () {
|
|
final PictureRecorder recorder = PictureRecorder();
|
|
final Canvas canvas = Canvas(recorder);
|
|
canvas.drawPaint(Paint());
|
|
final Picture picture = recorder.endRecording();
|
|
final PictureLayer layer = PictureLayer(const Rect.fromLTRB(0, 0, 1, 1));
|
|
layer.picture = picture;
|
|
layer.isComplexHint = true;
|
|
layer.willChangeHint = false;
|
|
final List<String> info = getDebugInfo(layer);
|
|
expect(info, contains('picture: ${describeIdentity(picture)}'));
|
|
expect(info, isNot(contains('engine layer: ${describeIdentity(null)}')));
|
|
expect(info, contains('raster cache hints: isComplex = true, willChange = false'));
|
|
});
|
|
|
|
test('Layer prints engineLayer if it is not null in debug info', () {
|
|
final ConcreteLayer layer = ConcreteLayer();
|
|
List<String> info = getDebugInfo(layer);
|
|
expect(info, isNot(contains('engine layer: ${describeIdentity(null)}')));
|
|
|
|
layer.engineLayer = FakeEngineLayer();
|
|
info = getDebugInfo(layer);
|
|
expect(info, contains('engine layer: ${describeIdentity(layer.engineLayer)}'));
|
|
});
|
|
|
|
test('mutating PictureLayer fields triggers needsAddToScene', () {
|
|
final PictureLayer pictureLayer = PictureLayer(Rect.zero);
|
|
checkNeedsAddToScene(pictureLayer, () {
|
|
final PictureRecorder recorder = PictureRecorder();
|
|
Canvas(recorder);
|
|
pictureLayer.picture = recorder.endRecording();
|
|
});
|
|
|
|
pictureLayer.isComplexHint = false;
|
|
checkNeedsAddToScene(pictureLayer, () {
|
|
pictureLayer.isComplexHint = true;
|
|
});
|
|
|
|
pictureLayer.willChangeHint = false;
|
|
checkNeedsAddToScene(pictureLayer, () {
|
|
pictureLayer.willChangeHint = true;
|
|
});
|
|
});
|
|
|
|
const Rect unitRect = Rect.fromLTRB(0, 0, 1, 1);
|
|
|
|
test('mutating PerformanceOverlayLayer fields triggers needsAddToScene', () {
|
|
final PerformanceOverlayLayer layer = PerformanceOverlayLayer(
|
|
overlayRect: Rect.zero,
|
|
optionsMask: 0,
|
|
rasterizerThreshold: 0,
|
|
checkerboardRasterCacheImages: false,
|
|
checkerboardOffscreenLayers: false,
|
|
);
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.overlayRect = unitRect;
|
|
});
|
|
});
|
|
|
|
test('mutating OffsetLayer fields triggers needsAddToScene', () {
|
|
final OffsetLayer layer = OffsetLayer();
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.offset = const Offset(1, 1);
|
|
});
|
|
});
|
|
|
|
test('mutating ClipRectLayer fields triggers needsAddToScene', () {
|
|
final ClipRectLayer layer = ClipRectLayer(clipRect: Rect.zero);
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.clipRect = unitRect;
|
|
});
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.clipBehavior = Clip.antiAliasWithSaveLayer;
|
|
});
|
|
});
|
|
|
|
test('mutating ClipRRectLayer fields triggers needsAddToScene', () {
|
|
final ClipRRectLayer layer = ClipRRectLayer(clipRRect: RRect.zero);
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.clipRRect = RRect.fromRectAndRadius(unitRect, Radius.zero);
|
|
});
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.clipBehavior = Clip.antiAliasWithSaveLayer;
|
|
});
|
|
});
|
|
|
|
test('mutating ClipPath fields triggers needsAddToScene', () {
|
|
final ClipPathLayer layer = ClipPathLayer(clipPath: Path());
|
|
checkNeedsAddToScene(layer, () {
|
|
final Path newPath = Path();
|
|
newPath.addRect(unitRect);
|
|
layer.clipPath = newPath;
|
|
});
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.clipBehavior = Clip.antiAliasWithSaveLayer;
|
|
});
|
|
});
|
|
|
|
test('mutating OpacityLayer fields triggers needsAddToScene', () {
|
|
final OpacityLayer layer = OpacityLayer(alpha: 0);
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.alpha = 1;
|
|
});
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.offset = const Offset(1, 1);
|
|
});
|
|
});
|
|
|
|
test('mutating ColorFilterLayer fields triggers needsAddToScene', () {
|
|
final ColorFilterLayer layer = ColorFilterLayer(
|
|
colorFilter: const ColorFilter.mode(Color(0xFFFF0000), BlendMode.color),
|
|
);
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.colorFilter = const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color);
|
|
});
|
|
});
|
|
|
|
test('mutating ShaderMaskLayer fields triggers needsAddToScene', () {
|
|
const Gradient gradient = RadialGradient(colors: <Color>[Color(0x00000000), Color(0x00000001)]);
|
|
final Shader shader = gradient.createShader(Rect.zero);
|
|
final ShaderMaskLayer layer = ShaderMaskLayer(shader: shader, maskRect: Rect.zero, blendMode: BlendMode.clear);
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.maskRect = unitRect;
|
|
});
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.blendMode = BlendMode.color;
|
|
});
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.shader = gradient.createShader(unitRect);
|
|
});
|
|
});
|
|
|
|
test('mutating BackdropFilterLayer fields triggers needsAddToScene', () {
|
|
final BackdropFilterLayer layer = BackdropFilterLayer(filter: ImageFilter.blur());
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.filter = ImageFilter.blur(sigmaX: 1.0);
|
|
});
|
|
});
|
|
|
|
test('mutating PhysicalModelLayer fields triggers needsAddToScene', () {
|
|
final PhysicalModelLayer layer = PhysicalModelLayer(
|
|
clipPath: Path(),
|
|
elevation: 0,
|
|
color: const Color(0x00000000),
|
|
shadowColor: const Color(0x00000000),
|
|
);
|
|
checkNeedsAddToScene(layer, () {
|
|
final Path newPath = Path();
|
|
newPath.addRect(unitRect);
|
|
layer.clipPath = newPath;
|
|
});
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.elevation = 1;
|
|
});
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.color = const Color(0x00000001);
|
|
});
|
|
checkNeedsAddToScene(layer, () {
|
|
layer.shadowColor = const Color(0x00000001);
|
|
});
|
|
});
|
|
|
|
test('ContainerLayer.toImage can render interior layer', () {
|
|
final OffsetLayer parent = OffsetLayer();
|
|
final OffsetLayer child = OffsetLayer();
|
|
final OffsetLayer grandChild = OffsetLayer();
|
|
child.append(grandChild);
|
|
parent.append(child);
|
|
|
|
// This renders the layers and generates engine layers.
|
|
parent.buildScene(SceneBuilder());
|
|
|
|
// Causes grandChild to pass its engine layer as `oldLayer`
|
|
grandChild.toImage(const Rect.fromLTRB(0, 0, 10, 10));
|
|
|
|
// Ensure we can render the same scene again after rendering an interior
|
|
// layer.
|
|
parent.buildScene(SceneBuilder());
|
|
}, skip: isBrowser); // TODO(yjbanov): `toImage` doesn't work on the Web: https://github.com/flutter/flutter/issues/49857
|
|
|
|
test('ContainerLayer.toImageSync can render interior layer', () {
|
|
final OffsetLayer parent = OffsetLayer();
|
|
final OffsetLayer child = OffsetLayer();
|
|
final OffsetLayer grandChild = OffsetLayer();
|
|
child.append(grandChild);
|
|
parent.append(child);
|
|
|
|
// This renders the layers and generates engine layers.
|
|
parent.buildScene(SceneBuilder());
|
|
|
|
// Causes grandChild to pass its engine layer as `oldLayer`
|
|
grandChild.toImageSync(const Rect.fromLTRB(0, 0, 10, 10));
|
|
|
|
// Ensure we can render the same scene again after rendering an interior
|
|
// layer.
|
|
parent.buildScene(SceneBuilder());
|
|
}, skip: isBrowser); // TODO(yjbanov): `toImage` doesn't work on the Web: https://github.com/flutter/flutter/issues/49857
|
|
|
|
test('PictureLayer does not let you call dispose unless refcount is 0', () {
|
|
PictureLayer layer = PictureLayer(Rect.zero);
|
|
expect(layer.debugHandleCount, 0);
|
|
layer.dispose();
|
|
expect(layer.debugDisposed, true);
|
|
|
|
layer = PictureLayer(Rect.zero);
|
|
final LayerHandle<PictureLayer> handle = LayerHandle<PictureLayer>(layer);
|
|
expect(layer.debugHandleCount, 1);
|
|
expect(() => layer.dispose(), throwsAssertionError);
|
|
handle.layer = null;
|
|
expect(layer.debugHandleCount, 0);
|
|
expect(layer.debugDisposed, true);
|
|
expect(() => layer.dispose(), throwsAssertionError); // already disposed.
|
|
});
|
|
|
|
test('Layer append/remove increases/decreases handle count', () {
|
|
final PictureLayer layer = PictureLayer(Rect.zero);
|
|
final ContainerLayer parent = ContainerLayer();
|
|
expect(layer.debugHandleCount, 0);
|
|
expect(layer.debugDisposed, false);
|
|
|
|
parent.append(layer);
|
|
expect(layer.debugHandleCount, 1);
|
|
expect(layer.debugDisposed, false);
|
|
|
|
layer.remove();
|
|
expect(layer.debugHandleCount, 0);
|
|
expect(layer.debugDisposed, true);
|
|
});
|
|
|
|
test('Layer.dispose disposes the engineLayer', () {
|
|
final Layer layer = ConcreteLayer();
|
|
final FakeEngineLayer engineLayer = FakeEngineLayer();
|
|
layer.engineLayer = engineLayer;
|
|
expect(engineLayer.disposed, false);
|
|
layer.dispose();
|
|
expect(engineLayer.disposed, true);
|
|
expect(layer.engineLayer, null);
|
|
});
|
|
|
|
test('Layer.engineLayer (set) disposes the engineLayer', () {
|
|
final Layer layer = ConcreteLayer();
|
|
final FakeEngineLayer engineLayer = FakeEngineLayer();
|
|
layer.engineLayer = engineLayer;
|
|
expect(engineLayer.disposed, false);
|
|
layer.engineLayer = null;
|
|
expect(engineLayer.disposed, true);
|
|
});
|
|
|
|
test('PictureLayer.picture (set) disposes the picture', () {
|
|
final PictureLayer layer = PictureLayer(Rect.zero);
|
|
final FakePicture picture = FakePicture();
|
|
layer.picture = picture;
|
|
expect(picture.disposed, false);
|
|
layer.picture = null;
|
|
expect(picture.disposed, true);
|
|
});
|
|
|
|
test('PictureLayer disposes the picture', () {
|
|
final PictureLayer layer = PictureLayer(Rect.zero);
|
|
final FakePicture picture = FakePicture();
|
|
layer.picture = picture;
|
|
expect(picture.disposed, false);
|
|
layer.dispose();
|
|
expect(picture.disposed, true);
|
|
});
|
|
|
|
test('LayerHandle disposes the layer', () {
|
|
final ConcreteLayer layer = ConcreteLayer();
|
|
final ConcreteLayer layer2 = ConcreteLayer();
|
|
|
|
expect(layer.debugHandleCount, 0);
|
|
expect(layer2.debugHandleCount, 0);
|
|
|
|
final LayerHandle<ConcreteLayer> holder = LayerHandle<ConcreteLayer>(layer);
|
|
expect(layer.debugHandleCount, 1);
|
|
expect(layer.debugDisposed, false);
|
|
expect(layer2.debugHandleCount, 0);
|
|
expect(layer2.debugDisposed, false);
|
|
|
|
holder.layer = layer;
|
|
expect(layer.debugHandleCount, 1);
|
|
expect(layer.debugDisposed, false);
|
|
expect(layer2.debugHandleCount, 0);
|
|
expect(layer2.debugDisposed, false);
|
|
|
|
holder.layer = layer2;
|
|
expect(layer.debugHandleCount, 0);
|
|
expect(layer.debugDisposed, true);
|
|
expect(layer2.debugHandleCount, 1);
|
|
expect(layer2.debugDisposed, false);
|
|
|
|
holder.layer = null;
|
|
expect(layer.debugHandleCount, 0);
|
|
expect(layer.debugDisposed, true);
|
|
expect(layer2.debugHandleCount, 0);
|
|
expect(layer2.debugDisposed, true);
|
|
|
|
expect(() => holder.layer = layer, throwsAssertionError);
|
|
});
|
|
|
|
test('OpacityLayer does not push an OffsetLayer if there are no children', () {
|
|
final OpacityLayer layer = OpacityLayer(alpha: 128);
|
|
final FakeSceneBuilder builder = FakeSceneBuilder();
|
|
layer.addToScene(builder);
|
|
expect(builder.pushedOpacity, false);
|
|
expect(builder.pushedOffset, false);
|
|
expect(builder.addedPicture, false);
|
|
expect(layer.engineLayer, null);
|
|
|
|
layer.append(PictureLayer(Rect.largest)..picture = FakePicture());
|
|
|
|
builder.reset();
|
|
layer.addToScene(builder);
|
|
|
|
expect(builder.pushedOpacity, true);
|
|
expect(builder.pushedOffset, false);
|
|
expect(builder.addedPicture, true);
|
|
expect(layer.engineLayer, isA<FakeOpacityEngineLayer>());
|
|
|
|
builder.reset();
|
|
|
|
layer.alpha = 200;
|
|
expect(layer.engineLayer, isA<FakeOpacityEngineLayer>());
|
|
|
|
layer.alpha = 255;
|
|
expect(layer.engineLayer, null);
|
|
|
|
builder.reset();
|
|
layer.addToScene(builder);
|
|
|
|
expect(builder.pushedOpacity, false);
|
|
expect(builder.pushedOffset, true);
|
|
expect(builder.addedPicture, true);
|
|
expect(layer.engineLayer, isA<FakeOffsetEngineLayer>());
|
|
|
|
layer.alpha = 200;
|
|
expect(layer.engineLayer, null);
|
|
|
|
builder.reset();
|
|
layer.addToScene(builder);
|
|
|
|
expect(builder.pushedOpacity, true);
|
|
expect(builder.pushedOffset, false);
|
|
expect(builder.addedPicture, true);
|
|
expect(layer.engineLayer, isA<FakeOpacityEngineLayer>());
|
|
});
|
|
|
|
test('OpacityLayer dispose its engineLayer if there are no children', () {
|
|
final OpacityLayer layer = OpacityLayer(alpha: 128);
|
|
final FakeSceneBuilder builder = FakeSceneBuilder();
|
|
layer.addToScene(builder);
|
|
expect(layer.engineLayer, null);
|
|
|
|
layer.append(PictureLayer(Rect.largest)..picture = FakePicture());
|
|
layer.addToScene(builder);
|
|
expect(layer.engineLayer, isA<FakeOpacityEngineLayer>());
|
|
|
|
layer.removeAllChildren();
|
|
layer.addToScene(builder);
|
|
expect(layer.engineLayer, null);
|
|
});
|
|
|
|
test('Layers describe clip bounds', () {
|
|
ContainerLayer layer = ContainerLayer();
|
|
expect(layer.describeClipBounds(), null);
|
|
|
|
const Rect bounds = Rect.fromLTRB(10, 10, 20, 20);
|
|
final RRect rbounds = RRect.fromRectXY(bounds, 2, 2);
|
|
layer = ClipRectLayer(clipRect: bounds);
|
|
expect(layer.describeClipBounds(), bounds);
|
|
|
|
layer = ClipRRectLayer(clipRRect: rbounds);
|
|
expect(layer.describeClipBounds(), rbounds.outerRect);
|
|
|
|
layer = ClipPathLayer(clipPath: Path()..addRect(bounds));
|
|
expect(layer.describeClipBounds(), bounds);
|
|
});
|
|
|
|
test('Subtree has composition callbacks', () {
|
|
final ContainerLayer root = ContainerLayer();
|
|
expect(root.subtreeHasCompositionCallbacks, false);
|
|
|
|
final List<VoidCallback> cancellationCallbacks = <VoidCallback>[];
|
|
|
|
cancellationCallbacks.add(root.addCompositionCallback((_) {}));
|
|
expect(root.subtreeHasCompositionCallbacks, true);
|
|
|
|
final ContainerLayer a1 = ContainerLayer();
|
|
final ContainerLayer a2 = ContainerLayer();
|
|
final ContainerLayer b1 = ContainerLayer();
|
|
root.append(a1);
|
|
root.append(a2);
|
|
a1.append(b1);
|
|
|
|
expect(root.subtreeHasCompositionCallbacks, true);
|
|
expect(a1.subtreeHasCompositionCallbacks, false);
|
|
expect(a2.subtreeHasCompositionCallbacks, false);
|
|
expect(b1.subtreeHasCompositionCallbacks, false);
|
|
cancellationCallbacks.add(b1.addCompositionCallback((_) {}));
|
|
|
|
expect(root.subtreeHasCompositionCallbacks, true);
|
|
expect(a1.subtreeHasCompositionCallbacks, true);
|
|
expect(a2.subtreeHasCompositionCallbacks, false);
|
|
expect(b1.subtreeHasCompositionCallbacks, true);
|
|
|
|
cancellationCallbacks.removeAt(0)();
|
|
|
|
expect(root.subtreeHasCompositionCallbacks, true);
|
|
expect(a1.subtreeHasCompositionCallbacks, true);
|
|
expect(a2.subtreeHasCompositionCallbacks, false);
|
|
expect(b1.subtreeHasCompositionCallbacks, true);
|
|
|
|
cancellationCallbacks.removeAt(0)();
|
|
|
|
expect(root.subtreeHasCompositionCallbacks, false);
|
|
expect(a1.subtreeHasCompositionCallbacks, false);
|
|
expect(a2.subtreeHasCompositionCallbacks, false);
|
|
expect(b1.subtreeHasCompositionCallbacks, false);
|
|
});
|
|
|
|
test('Subtree has composition callbacks - removeChild', () {
|
|
final ContainerLayer root = ContainerLayer();
|
|
expect(root.subtreeHasCompositionCallbacks, false);
|
|
|
|
final ContainerLayer a1 = ContainerLayer();
|
|
final ContainerLayer a2 = ContainerLayer();
|
|
final ContainerLayer b1 = ContainerLayer();
|
|
root.append(a1);
|
|
root.append(a2);
|
|
a1.append(b1);
|
|
|
|
expect(b1.subtreeHasCompositionCallbacks, false);
|
|
expect(a1.subtreeHasCompositionCallbacks, false);
|
|
expect(root.subtreeHasCompositionCallbacks, false);
|
|
expect(a2.subtreeHasCompositionCallbacks, false);
|
|
|
|
b1.addCompositionCallback((_) { });
|
|
|
|
expect(b1.subtreeHasCompositionCallbacks, true);
|
|
expect(a1.subtreeHasCompositionCallbacks, true);
|
|
expect(root.subtreeHasCompositionCallbacks, true);
|
|
expect(a2.subtreeHasCompositionCallbacks, false);
|
|
|
|
b1.remove();
|
|
|
|
expect(b1.subtreeHasCompositionCallbacks, true);
|
|
expect(a1.subtreeHasCompositionCallbacks, false);
|
|
expect(root.subtreeHasCompositionCallbacks, false);
|
|
expect(a2.subtreeHasCompositionCallbacks, false);
|
|
});
|
|
|
|
test('No callback if removed', () {
|
|
final ContainerLayer root = ContainerLayer();
|
|
|
|
final ContainerLayer a1 = ContainerLayer();
|
|
final ContainerLayer a2 = ContainerLayer();
|
|
final ContainerLayer b1 = ContainerLayer();
|
|
root.append(a1);
|
|
root.append(a2);
|
|
a1.append(b1);
|
|
|
|
// Add and immediately remove the callback.
|
|
b1.addCompositionCallback((Layer layer) {
|
|
fail('Should not have called back');
|
|
})();
|
|
|
|
root.buildScene(SceneBuilder()).dispose();
|
|
});
|
|
|
|
test('Observe layer tree composition - not retained', () {
|
|
final ContainerLayer root = ContainerLayer();
|
|
|
|
final ContainerLayer a1 = ContainerLayer();
|
|
final ContainerLayer a2 = ContainerLayer();
|
|
final ContainerLayer b1 = ContainerLayer();
|
|
root.append(a1);
|
|
root.append(a2);
|
|
a1.append(b1);
|
|
|
|
bool compositedB1 = false;
|
|
|
|
b1.addCompositionCallback((Layer layer) {
|
|
expect(layer, b1);
|
|
compositedB1 = true;
|
|
});
|
|
|
|
expect(compositedB1, false);
|
|
|
|
root.buildScene(SceneBuilder()).dispose();
|
|
|
|
expect(compositedB1, true);
|
|
});
|
|
|
|
test('Observe layer tree composition - retained', () {
|
|
final ContainerLayer root = ContainerLayer();
|
|
|
|
final ContainerLayer a1 = ContainerLayer();
|
|
final ContainerLayer a2 = ContainerLayer();
|
|
final ContainerLayer b1 = ContainerLayer();
|
|
root.append(a1);
|
|
root.append(a2);
|
|
a1.append(b1);
|
|
|
|
// Actually build the retained layer so that the engine sees it as real and
|
|
// reusable.
|
|
SceneBuilder builder = SceneBuilder();
|
|
b1.engineLayer = builder.pushOffset(0, 0);
|
|
builder.build().dispose();
|
|
builder = SceneBuilder();
|
|
|
|
// Force the layer to appear clean and have an engine layer for retained
|
|
// rendering.
|
|
expect(b1.engineLayer, isNotNull);
|
|
b1.debugMarkClean();
|
|
expect(b1.debugSubtreeNeedsAddToScene, false);
|
|
|
|
bool compositedB1 = false;
|
|
|
|
b1.addCompositionCallback((Layer layer) {
|
|
expect(layer, b1);
|
|
compositedB1 = true;
|
|
});
|
|
|
|
expect(compositedB1, false);
|
|
|
|
root.buildScene(builder).dispose();
|
|
|
|
expect(compositedB1, true);
|
|
});
|
|
|
|
test('Observe layer tree composition - asserts on mutation', () {
|
|
final ContainerLayer root = ContainerLayer();
|
|
|
|
final ContainerLayer a1 = ContainerLayer();
|
|
final ContainerLayer a2 = ContainerLayer();
|
|
final ContainerLayer b1 = ContainerLayer();
|
|
root.append(a1);
|
|
root.append(a2);
|
|
a1.append(b1);
|
|
|
|
bool compositedB1 = false;
|
|
|
|
b1.addCompositionCallback((Layer layer) {
|
|
expect(layer, b1);
|
|
expect(() => layer.remove(), throwsAssertionError);
|
|
expect(() => layer.dispose(), throwsAssertionError);
|
|
expect(() => layer.markNeedsAddToScene(), throwsAssertionError);
|
|
expect(() => layer.debugMarkClean(), throwsAssertionError);
|
|
expect(() => layer.updateSubtreeNeedsAddToScene(), throwsAssertionError);
|
|
expect(() => layer.dropChild(ContainerLayer()), throwsAssertionError);
|
|
expect(() => layer.adoptChild(ContainerLayer()), throwsAssertionError);
|
|
expect(() => (layer as ContainerLayer).append(ContainerLayer()), throwsAssertionError);
|
|
expect(() => layer.engineLayer = null, throwsAssertionError);
|
|
compositedB1 = true;
|
|
});
|
|
|
|
expect(compositedB1, false);
|
|
|
|
root.buildScene(SceneBuilder()).dispose();
|
|
|
|
expect(compositedB1, true);
|
|
});
|
|
|
|
test('Observe layer tree composition - detach triggers callback', () {
|
|
final ContainerLayer root = ContainerLayer();
|
|
|
|
final ContainerLayer a1 = ContainerLayer();
|
|
final ContainerLayer a2 = ContainerLayer();
|
|
final ContainerLayer b1 = ContainerLayer();
|
|
root.append(a1);
|
|
root.append(a2);
|
|
a1.append(b1);
|
|
|
|
bool compositedB1 = false;
|
|
|
|
b1.addCompositionCallback((Layer layer) {
|
|
expect(layer, b1);
|
|
compositedB1 = true;
|
|
});
|
|
|
|
root.attach(Object());
|
|
expect(compositedB1, false);
|
|
root.detach();
|
|
expect(compositedB1, true);
|
|
});
|
|
|
|
test('Observe layer tree composition - observer count correctly maintained', () {
|
|
final ContainerLayer root = ContainerLayer();
|
|
final ContainerLayer a1 = ContainerLayer();
|
|
root.append(a1);
|
|
|
|
expect(root.subtreeHasCompositionCallbacks, false);
|
|
expect(a1.subtreeHasCompositionCallbacks, false);
|
|
|
|
final VoidCallback remover1 = a1.addCompositionCallback((_) { });
|
|
final VoidCallback remover2 = a1.addCompositionCallback((_) { });
|
|
|
|
expect(root.subtreeHasCompositionCallbacks, true);
|
|
expect(a1.subtreeHasCompositionCallbacks, true);
|
|
|
|
remover1();
|
|
|
|
expect(root.subtreeHasCompositionCallbacks, true);
|
|
expect(a1.subtreeHasCompositionCallbacks, true);
|
|
|
|
remover2();
|
|
|
|
expect(root.subtreeHasCompositionCallbacks, false);
|
|
expect(a1.subtreeHasCompositionCallbacks, false);
|
|
});
|
|
|
|
test('Double removing a observe callback throws', () {
|
|
final ContainerLayer root = ContainerLayer();
|
|
final VoidCallback callback = root.addCompositionCallback((_) { });
|
|
callback();
|
|
|
|
expect(() => callback(), throwsAssertionError);
|
|
});
|
|
|
|
test('Removing an observe callback on a disposed layer does not throw', () {
|
|
final ContainerLayer root = ContainerLayer();
|
|
final VoidCallback callback = root.addCompositionCallback((_) { });
|
|
root.dispose();
|
|
expect(() => callback(), returnsNormally);
|
|
});
|
|
|
|
test('Layer types that support rasterization', () {
|
|
// Supported.
|
|
final OffsetLayer offsetLayer = OffsetLayer();
|
|
final OpacityLayer opacityLayer = OpacityLayer();
|
|
final ClipRectLayer clipRectLayer = ClipRectLayer();
|
|
final ClipRRectLayer clipRRectLayer = ClipRRectLayer();
|
|
final ImageFilterLayer imageFilterLayer = ImageFilterLayer();
|
|
final BackdropFilterLayer backdropFilterLayer = BackdropFilterLayer();
|
|
final PhysicalModelLayer physicalModelLayer = PhysicalModelLayer();
|
|
final ColorFilterLayer colorFilterLayer = ColorFilterLayer();
|
|
final ShaderMaskLayer shaderMaskLayer = ShaderMaskLayer();
|
|
final TextureLayer textureLayer = TextureLayer(rect: Rect.zero, textureId: 1);
|
|
expect(offsetLayer.supportsRasterization(), true);
|
|
expect(opacityLayer.supportsRasterization(), true);
|
|
expect(clipRectLayer.supportsRasterization(), true);
|
|
expect(clipRRectLayer.supportsRasterization(), true);
|
|
expect(imageFilterLayer.supportsRasterization(), true);
|
|
expect(backdropFilterLayer.supportsRasterization(), true);
|
|
expect(physicalModelLayer.supportsRasterization(), true);
|
|
expect(colorFilterLayer.supportsRasterization(), true);
|
|
expect(shaderMaskLayer.supportsRasterization(), true);
|
|
expect(textureLayer.supportsRasterization(), true);
|
|
|
|
// Unsupported.
|
|
final PlatformViewLayer platformViewLayer = PlatformViewLayer(rect: Rect.zero, viewId: 1);
|
|
|
|
expect(platformViewLayer.supportsRasterization(), false);
|
|
});
|
|
}
|
|
|
|
class FakeEngineLayer extends Fake implements EngineLayer {
|
|
bool disposed = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
assert(!disposed);
|
|
disposed = true;
|
|
}
|
|
}
|
|
|
|
class FakePicture extends Fake implements Picture {
|
|
bool disposed = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
assert(!disposed);
|
|
disposed = true;
|
|
}
|
|
}
|
|
|
|
class ConcreteLayer extends Layer {
|
|
@override
|
|
void addToScene(SceneBuilder builder) {}
|
|
}
|
|
|
|
class _TestAlwaysNeedsAddToSceneLayer extends ContainerLayer {
|
|
@override
|
|
bool get alwaysNeedsAddToScene => true;
|
|
}
|
|
|
|
class FakeSceneBuilder extends Fake implements SceneBuilder {
|
|
void reset() {
|
|
pushedOpacity = false;
|
|
pushedOffset = false;
|
|
addedPicture = false;
|
|
}
|
|
|
|
bool pushedOpacity = false;
|
|
bool pushedOffset = false;
|
|
bool addedPicture = false;
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) {
|
|
// Use noSuchMethod forwarding instead of override these methods to make it easier
|
|
// for these methods to add new optional arguments in the future.
|
|
switch (invocation.memberName) {
|
|
case #pushOpacity:
|
|
pushedOpacity = true;
|
|
return FakeOpacityEngineLayer();
|
|
case #pushOffset:
|
|
pushedOffset = true;
|
|
return FakeOffsetEngineLayer();
|
|
case #addPicture:
|
|
addedPicture = true;
|
|
return;
|
|
case #pop:
|
|
return;
|
|
}
|
|
super.noSuchMethod(invocation);
|
|
}
|
|
}
|
|
|
|
class FakeOpacityEngineLayer extends FakeEngineLayer implements OpacityEngineLayer {}
|
|
|
|
class FakeOffsetEngineLayer extends FakeEngineLayer implements OffsetEngineLayer {}
|