mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Some render box subclasses have a specific layout contract that is tightly coupled with other render box subclasses (e.g. two private classes in a local project file). In these cases, it is also possible that they use a constraints object that is a subclass of `BoxConstraints`. To allow for this, this change makes the `constraints` argument to `RenderBox.computeDryLayout()` a covariant argument. For completeness' sake, this updates the other render objects in the rendering package to also use the covariant keyword for this argument.
1113 lines
42 KiB
Dart
1113 lines
42 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' as ui show Gradient, Image, ImageFilter;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'rendering_tester.dart';
|
|
|
|
void main() {
|
|
TestRenderingFlutterBinding.ensureInitialized();
|
|
test('RenderFittedBox handles applying paint transform and hit-testing with empty size', () {
|
|
final RenderFittedBox fittedBox = RenderFittedBox(
|
|
child: RenderCustomPaint(
|
|
painter: TestCallbackPainter(onPaint: () {}),
|
|
),
|
|
);
|
|
|
|
layout(fittedBox, phase: EnginePhase.flushSemantics);
|
|
final Matrix4 transform = Matrix4.identity();
|
|
fittedBox.applyPaintTransform(fittedBox.child!, transform);
|
|
expect(transform, Matrix4.zero());
|
|
|
|
final BoxHitTestResult hitTestResult = BoxHitTestResult();
|
|
expect(fittedBox.hitTestChildren(hitTestResult, position: Offset.zero), isFalse);
|
|
});
|
|
|
|
test('RenderFittedBox does not paint with empty sizes', () {
|
|
bool painted;
|
|
RenderFittedBox makeFittedBox(Size size) {
|
|
return RenderFittedBox(
|
|
child: RenderCustomPaint(
|
|
preferredSize: size,
|
|
painter: TestCallbackPainter(onPaint: () {
|
|
painted = true;
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
// The RenderFittedBox paints if both its size and its child's size are nonempty.
|
|
painted = false;
|
|
layout(makeFittedBox(const Size(1, 1)), phase: EnginePhase.paint);
|
|
expect(painted, equals(true));
|
|
|
|
// The RenderFittedBox should not paint if its child is empty-sized.
|
|
painted = false;
|
|
layout(makeFittedBox(Size.zero), phase: EnginePhase.paint);
|
|
expect(painted, equals(false));
|
|
|
|
// The RenderFittedBox should not paint if it is empty.
|
|
painted = false;
|
|
layout(makeFittedBox(const Size(1, 1)), constraints: BoxConstraints.tight(Size.zero), phase: EnginePhase.paint);
|
|
expect(painted, equals(false));
|
|
});
|
|
|
|
test('RenderPhysicalModel compositing', () {
|
|
final RenderPhysicalModel root = RenderPhysicalModel(color: const Color(0xffff00ff));
|
|
layout(root, phase: EnginePhase.composite);
|
|
expect(root.needsCompositing, isFalse);
|
|
|
|
// On Fuchsia, the system compositor is responsible for drawing shadows
|
|
// for physical model layers with non-zero elevation.
|
|
root.elevation = 1.0;
|
|
pumpFrame(phase: EnginePhase.composite);
|
|
expect(root.needsCompositing, isFalse);
|
|
|
|
root.elevation = 0.0;
|
|
pumpFrame(phase: EnginePhase.composite);
|
|
expect(root.needsCompositing, isFalse);
|
|
});
|
|
|
|
test('RenderSemanticsGestureHandler adds/removes correct semantic actions', () {
|
|
final RenderSemanticsGestureHandler renderObj = RenderSemanticsGestureHandler(
|
|
onTap: () { },
|
|
onHorizontalDragUpdate: (DragUpdateDetails details) { },
|
|
);
|
|
|
|
SemanticsConfiguration config = SemanticsConfiguration();
|
|
renderObj.describeSemanticsConfiguration(config);
|
|
expect(config.getActionHandler(SemanticsAction.tap), isNotNull);
|
|
expect(config.getActionHandler(SemanticsAction.scrollLeft), isNotNull);
|
|
expect(config.getActionHandler(SemanticsAction.scrollRight), isNotNull);
|
|
|
|
config = SemanticsConfiguration();
|
|
renderObj.validActions = <SemanticsAction>{SemanticsAction.tap, SemanticsAction.scrollLeft};
|
|
|
|
renderObj.describeSemanticsConfiguration(config);
|
|
expect(config.getActionHandler(SemanticsAction.tap), isNotNull);
|
|
expect(config.getActionHandler(SemanticsAction.scrollLeft), isNotNull);
|
|
expect(config.getActionHandler(SemanticsAction.scrollRight), isNull);
|
|
});
|
|
|
|
group('RenderPhysicalShape', () {
|
|
test('shape change triggers repaint', () {
|
|
for (final TargetPlatform platform in TargetPlatform.values) {
|
|
debugDefaultTargetPlatformOverride = platform;
|
|
|
|
final RenderPhysicalShape root = RenderPhysicalShape(
|
|
color: const Color(0xffff00ff),
|
|
clipper: const ShapeBorderClipper(shape: CircleBorder()),
|
|
);
|
|
layout(root, phase: EnginePhase.composite);
|
|
expect(root.debugNeedsPaint, isFalse);
|
|
|
|
// Same shape, no repaint.
|
|
root.clipper = const ShapeBorderClipper(shape: CircleBorder());
|
|
expect(root.debugNeedsPaint, isFalse);
|
|
|
|
// Different shape triggers repaint.
|
|
root.clipper = const ShapeBorderClipper(shape: StadiumBorder());
|
|
expect(root.debugNeedsPaint, isTrue);
|
|
}
|
|
debugDefaultTargetPlatformOverride = null;
|
|
});
|
|
|
|
test('compositing', () {
|
|
for (final TargetPlatform platform in TargetPlatform.values) {
|
|
debugDefaultTargetPlatformOverride = platform;
|
|
final RenderPhysicalShape root = RenderPhysicalShape(
|
|
color: const Color(0xffff00ff),
|
|
clipper: const ShapeBorderClipper(shape: CircleBorder()),
|
|
);
|
|
layout(root, phase: EnginePhase.composite);
|
|
expect(root.needsCompositing, isFalse);
|
|
|
|
// On non-Fuchsia platforms, we composite physical shape layers
|
|
root.elevation = 1.0;
|
|
pumpFrame(phase: EnginePhase.composite);
|
|
expect(root.needsCompositing, isFalse);
|
|
|
|
root.elevation = 0.0;
|
|
pumpFrame(phase: EnginePhase.composite);
|
|
expect(root.needsCompositing, isFalse);
|
|
}
|
|
debugDefaultTargetPlatformOverride = null;
|
|
});
|
|
});
|
|
|
|
test('RenderRepaintBoundary can capture images of itself', () async {
|
|
RenderRepaintBoundary boundary = RenderRepaintBoundary();
|
|
layout(boundary, constraints: BoxConstraints.tight(const Size(100.0, 200.0)));
|
|
pumpFrame(phase: EnginePhase.composite);
|
|
ui.Image image = await boundary.toImage();
|
|
expect(image.width, equals(100));
|
|
expect(image.height, equals(200));
|
|
|
|
// Now with pixel ratio set to something other than 1.0.
|
|
boundary = RenderRepaintBoundary();
|
|
layout(boundary, constraints: BoxConstraints.tight(const Size(100.0, 200.0)));
|
|
pumpFrame(phase: EnginePhase.composite);
|
|
image = await boundary.toImage(pixelRatio: 2.0);
|
|
expect(image.width, equals(200));
|
|
expect(image.height, equals(400));
|
|
|
|
// Try building one with two child layers and make sure it renders them both.
|
|
boundary = RenderRepaintBoundary();
|
|
final RenderStack stack = RenderStack()..alignment = Alignment.topLeft;
|
|
final RenderDecoratedBox blackBox = RenderDecoratedBox(
|
|
decoration: const BoxDecoration(color: Color(0xff000000)),
|
|
child: RenderConstrainedBox(
|
|
additionalConstraints: BoxConstraints.tight(const Size.square(20.0)),
|
|
),
|
|
);
|
|
stack.add(
|
|
RenderOpacity()
|
|
..opacity = 0.5
|
|
..child = blackBox,
|
|
);
|
|
final RenderDecoratedBox whiteBox = RenderDecoratedBox(
|
|
decoration: const BoxDecoration(color: Color(0xffffffff)),
|
|
child: RenderConstrainedBox(
|
|
additionalConstraints: BoxConstraints.tight(const Size.square(10.0)),
|
|
),
|
|
);
|
|
final RenderPositionedBox positioned = RenderPositionedBox(
|
|
widthFactor: 2.0,
|
|
heightFactor: 2.0,
|
|
alignment: Alignment.topRight,
|
|
child: whiteBox,
|
|
);
|
|
stack.add(positioned);
|
|
boundary.child = stack;
|
|
layout(boundary, constraints: BoxConstraints.tight(const Size(20.0, 20.0)));
|
|
pumpFrame(phase: EnginePhase.composite);
|
|
image = await boundary.toImage();
|
|
expect(image.width, equals(20));
|
|
expect(image.height, equals(20));
|
|
ByteData data = (await image.toByteData())!;
|
|
|
|
int getPixel(int x, int y) => data.getUint32((x + y * image.width) * 4);
|
|
|
|
expect(data.lengthInBytes, equals(20 * 20 * 4));
|
|
expect(data.elementSizeInBytes, equals(1));
|
|
expect(getPixel(0, 0), equals(0x00000080));
|
|
expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));
|
|
|
|
final OffsetLayer layer = boundary.debugLayer! as OffsetLayer;
|
|
|
|
image = await layer.toImage(Offset.zero & const Size(20.0, 20.0));
|
|
expect(image.width, equals(20));
|
|
expect(image.height, equals(20));
|
|
data = (await image.toByteData())!;
|
|
expect(getPixel(0, 0), equals(0x00000080));
|
|
expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));
|
|
|
|
// non-zero offsets.
|
|
image = await layer.toImage(const Offset(-10.0, -10.0) & const Size(30.0, 30.0));
|
|
expect(image.width, equals(30));
|
|
expect(image.height, equals(30));
|
|
data = (await image.toByteData())!;
|
|
expect(getPixel(0, 0), equals(0x00000000));
|
|
expect(getPixel(10, 10), equals(0x00000080));
|
|
expect(getPixel(image.width - 1, 0), equals(0x00000000));
|
|
expect(getPixel(image.width - 1, 10), equals(0xffffffff));
|
|
|
|
// offset combined with a custom pixel ratio.
|
|
image = await layer.toImage(const Offset(-10.0, -10.0) & const Size(30.0, 30.0), pixelRatio: 2.0);
|
|
expect(image.width, equals(60));
|
|
expect(image.height, equals(60));
|
|
data = (await image.toByteData())!;
|
|
expect(getPixel(0, 0), equals(0x00000000));
|
|
expect(getPixel(20, 20), equals(0x00000080));
|
|
expect(getPixel(image.width - 1, 0), equals(0x00000000));
|
|
expect(getPixel(image.width - 1, 20), equals(0xffffffff));
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/49857
|
|
|
|
test('RenderRepaintBoundary can capture images of itself synchronously', () async {
|
|
RenderRepaintBoundary boundary = RenderRepaintBoundary();
|
|
layout(boundary, constraints: BoxConstraints.tight(const Size(100.0, 200.0)));
|
|
pumpFrame(phase: EnginePhase.composite);
|
|
ui.Image image = boundary.toImageSync();
|
|
expect(image.width, equals(100));
|
|
expect(image.height, equals(200));
|
|
|
|
// Now with pixel ratio set to something other than 1.0.
|
|
boundary = RenderRepaintBoundary();
|
|
layout(boundary, constraints: BoxConstraints.tight(const Size(100.0, 200.0)));
|
|
pumpFrame(phase: EnginePhase.composite);
|
|
image = boundary.toImageSync(pixelRatio: 2.0);
|
|
expect(image.width, equals(200));
|
|
expect(image.height, equals(400));
|
|
|
|
// Try building one with two child layers and make sure it renders them both.
|
|
boundary = RenderRepaintBoundary();
|
|
final RenderStack stack = RenderStack()..alignment = Alignment.topLeft;
|
|
final RenderDecoratedBox blackBox = RenderDecoratedBox(
|
|
decoration: const BoxDecoration(color: Color(0xff000000)),
|
|
child: RenderConstrainedBox(
|
|
additionalConstraints: BoxConstraints.tight(const Size.square(20.0)),
|
|
),
|
|
);
|
|
stack.add(
|
|
RenderOpacity()
|
|
..opacity = 0.5
|
|
..child = blackBox,
|
|
);
|
|
final RenderDecoratedBox whiteBox = RenderDecoratedBox(
|
|
decoration: const BoxDecoration(color: Color(0xffffffff)),
|
|
child: RenderConstrainedBox(
|
|
additionalConstraints: BoxConstraints.tight(const Size.square(10.0)),
|
|
),
|
|
);
|
|
final RenderPositionedBox positioned = RenderPositionedBox(
|
|
widthFactor: 2.0,
|
|
heightFactor: 2.0,
|
|
alignment: Alignment.topRight,
|
|
child: whiteBox,
|
|
);
|
|
stack.add(positioned);
|
|
boundary.child = stack;
|
|
layout(boundary, constraints: BoxConstraints.tight(const Size(20.0, 20.0)));
|
|
pumpFrame(phase: EnginePhase.composite);
|
|
image = boundary.toImageSync();
|
|
expect(image.width, equals(20));
|
|
expect(image.height, equals(20));
|
|
ByteData data = (await image.toByteData())!;
|
|
|
|
int getPixel(int x, int y) => data.getUint32((x + y * image.width) * 4);
|
|
|
|
expect(data.lengthInBytes, equals(20 * 20 * 4));
|
|
expect(data.elementSizeInBytes, equals(1));
|
|
expect(getPixel(0, 0), equals(0x00000080));
|
|
expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));
|
|
|
|
final OffsetLayer layer = boundary.debugLayer! as OffsetLayer;
|
|
|
|
image = layer.toImageSync(Offset.zero & const Size(20.0, 20.0));
|
|
expect(image.width, equals(20));
|
|
expect(image.height, equals(20));
|
|
data = (await image.toByteData())!;
|
|
expect(getPixel(0, 0), equals(0x00000080));
|
|
expect(getPixel(image.width - 1, 0 ), equals(0xffffffff));
|
|
|
|
// non-zero offsets.
|
|
image = layer.toImageSync(const Offset(-10.0, -10.0) & const Size(30.0, 30.0));
|
|
expect(image.width, equals(30));
|
|
expect(image.height, equals(30));
|
|
data = (await image.toByteData())!;
|
|
expect(getPixel(0, 0), equals(0x00000000));
|
|
expect(getPixel(10, 10), equals(0x00000080));
|
|
expect(getPixel(image.width - 1, 0), equals(0x00000000));
|
|
expect(getPixel(image.width - 1, 10), equals(0xffffffff));
|
|
|
|
// offset combined with a custom pixel ratio.
|
|
image = layer.toImageSync(const Offset(-10.0, -10.0) & const Size(30.0, 30.0), pixelRatio: 2.0);
|
|
expect(image.width, equals(60));
|
|
expect(image.height, equals(60));
|
|
data = (await image.toByteData())!;
|
|
expect(getPixel(0, 0), equals(0x00000000));
|
|
expect(getPixel(20, 20), equals(0x00000080));
|
|
expect(getPixel(image.width - 1, 0), equals(0x00000000));
|
|
expect(getPixel(image.width - 1, 20), equals(0xffffffff));
|
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/49857
|
|
|
|
test('RenderOpacity does not composite if it is transparent', () {
|
|
final RenderOpacity renderOpacity = RenderOpacity(
|
|
opacity: 0.0,
|
|
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
|
|
);
|
|
|
|
layout(renderOpacity, phase: EnginePhase.composite);
|
|
expect(renderOpacity.needsCompositing, false);
|
|
});
|
|
|
|
test('RenderOpacity does composite if it is opaque', () {
|
|
final RenderOpacity renderOpacity = RenderOpacity(
|
|
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
|
|
);
|
|
|
|
layout(renderOpacity, phase: EnginePhase.composite);
|
|
expect(renderOpacity.needsCompositing, true);
|
|
});
|
|
|
|
test('RenderOpacity does composite if it is partially opaque', () {
|
|
final RenderOpacity renderOpacity = RenderOpacity(
|
|
opacity: 0.1,
|
|
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
|
|
);
|
|
|
|
layout(renderOpacity, phase: EnginePhase.composite);
|
|
expect(renderOpacity.needsCompositing, true);
|
|
});
|
|
|
|
test('RenderOpacity reuses its layer', () {
|
|
_testLayerReuse<OpacityLayer>(RenderOpacity(
|
|
opacity: 0.5, // must not be 0 or 1.0. Otherwise, it won't create a layer
|
|
child: RenderRepaintBoundary(
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
), // size doesn't matter
|
|
));
|
|
});
|
|
|
|
test('RenderAnimatedOpacity does not composite if it is transparent', () async {
|
|
final Animation<double> opacityAnimation = AnimationController(
|
|
vsync: FakeTickerProvider(),
|
|
)..value = 0.0;
|
|
|
|
final RenderAnimatedOpacity renderAnimatedOpacity = RenderAnimatedOpacity(
|
|
opacity: opacityAnimation,
|
|
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
|
|
);
|
|
|
|
layout(renderAnimatedOpacity, phase: EnginePhase.composite);
|
|
expect(renderAnimatedOpacity.needsCompositing, false);
|
|
});
|
|
|
|
test('RenderAnimatedOpacity does composite if it is opaque', () {
|
|
final Animation<double> opacityAnimation = AnimationController(
|
|
vsync: FakeTickerProvider(),
|
|
)..value = 1.0;
|
|
|
|
final RenderAnimatedOpacity renderAnimatedOpacity = RenderAnimatedOpacity(
|
|
opacity: opacityAnimation,
|
|
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
|
|
);
|
|
|
|
layout(renderAnimatedOpacity, phase: EnginePhase.composite);
|
|
expect(renderAnimatedOpacity.needsCompositing, true);
|
|
});
|
|
|
|
test('RenderAnimatedOpacity does composite if it is partially opaque', () {
|
|
final Animation<double> opacityAnimation = AnimationController(
|
|
vsync: FakeTickerProvider(),
|
|
)..value = 0.5;
|
|
|
|
final RenderAnimatedOpacity renderAnimatedOpacity = RenderAnimatedOpacity(
|
|
opacity: opacityAnimation,
|
|
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
|
|
);
|
|
|
|
layout(renderAnimatedOpacity, phase: EnginePhase.composite);
|
|
expect(renderAnimatedOpacity.needsCompositing, true);
|
|
});
|
|
|
|
test('RenderAnimatedOpacity reuses its layer', () {
|
|
final Animation<double> opacityAnimation = AnimationController(
|
|
vsync: FakeTickerProvider(),
|
|
)..value = 0.5; // must not be 0 or 1.0. Otherwise, it won't create a layer
|
|
|
|
_testLayerReuse<OpacityLayer>(RenderAnimatedOpacity(
|
|
opacity: opacityAnimation,
|
|
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
|
|
));
|
|
});
|
|
|
|
test('RenderShaderMask reuses its layer', () {
|
|
_testLayerReuse<ShaderMaskLayer>(RenderShaderMask(
|
|
shaderCallback: (Rect rect) {
|
|
return ui.Gradient.radial(
|
|
rect.center,
|
|
rect.shortestSide / 2.0,
|
|
const <Color>[Color.fromRGBO(0, 0, 0, 1.0), Color.fromRGBO(255, 255, 255, 1.0)],
|
|
);
|
|
},
|
|
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
|
|
));
|
|
});
|
|
|
|
test('RenderBackdropFilter reuses its layer', () {
|
|
_testLayerReuse<BackdropFilterLayer>(RenderBackdropFilter(
|
|
filter: ui.ImageFilter.blur(),
|
|
child: RenderSizedBox(const Size(1.0, 1.0)), // size doesn't matter
|
|
));
|
|
});
|
|
|
|
test('RenderClipRect reuses its layer', () {
|
|
_testLayerReuse<ClipRectLayer>(RenderClipRect(
|
|
clipper: _TestRectClipper(),
|
|
child: RenderRepaintBoundary(
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
), // size doesn't matter
|
|
));
|
|
});
|
|
|
|
test('RenderClipRRect reuses its layer', () {
|
|
_testLayerReuse<ClipRRectLayer>(RenderClipRRect(
|
|
clipper: _TestRRectClipper(),
|
|
child: RenderRepaintBoundary(
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
), // size doesn't matter
|
|
));
|
|
});
|
|
|
|
test('RenderClipOval reuses its layer', () {
|
|
_testLayerReuse<ClipPathLayer>(RenderClipOval(
|
|
clipper: _TestRectClipper(),
|
|
child: RenderRepaintBoundary(
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
), // size doesn't matter
|
|
));
|
|
});
|
|
|
|
test('RenderClipPath reuses its layer', () {
|
|
_testLayerReuse<ClipPathLayer>(RenderClipPath(
|
|
clipper: _TestPathClipper(),
|
|
child: RenderRepaintBoundary(
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
), // size doesn't matter
|
|
));
|
|
});
|
|
|
|
test('RenderPhysicalModel reuses its layer', () {
|
|
_testLayerReuse<ClipRRectLayer>(RenderPhysicalModel(
|
|
clipBehavior: Clip.hardEdge,
|
|
color: const Color.fromRGBO(0, 0, 0, 1.0),
|
|
child: RenderRepaintBoundary(
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
), // size doesn't matter
|
|
));
|
|
});
|
|
|
|
test('RenderPhysicalShape reuses its layer', () {
|
|
_testLayerReuse<ClipPathLayer>(RenderPhysicalShape(
|
|
clipper: _TestPathClipper(),
|
|
clipBehavior: Clip.hardEdge,
|
|
color: const Color.fromRGBO(0, 0, 0, 1.0),
|
|
child: RenderRepaintBoundary(
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
), // size doesn't matter
|
|
));
|
|
});
|
|
|
|
test('RenderTransform reuses its layer', () {
|
|
_testLayerReuse<TransformLayer>(RenderTransform(
|
|
// Use a 3D transform to force compositing.
|
|
transform: Matrix4.rotationX(0.1),
|
|
child: RenderRepaintBoundary(
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
), // size doesn't matter
|
|
));
|
|
});
|
|
|
|
void testFittedBoxWithClipRectLayer() {
|
|
_testLayerReuse<ClipRectLayer>(RenderFittedBox(
|
|
fit: BoxFit.cover,
|
|
clipBehavior: Clip.hardEdge,
|
|
// Inject opacity under the clip to force compositing.
|
|
child: RenderRepaintBoundary(
|
|
child: RenderSizedBox(const Size(100.0, 200.0)),
|
|
), // size doesn't matter
|
|
));
|
|
}
|
|
|
|
void testFittedBoxWithTransformLayer() {
|
|
_testLayerReuse<TransformLayer>(RenderFittedBox(
|
|
fit: BoxFit.fill,
|
|
// Inject opacity under the clip to force compositing.
|
|
child: RenderRepaintBoundary(
|
|
child: RenderSizedBox(const Size(1, 1)),
|
|
), // size doesn't matter
|
|
));
|
|
}
|
|
|
|
test('RenderFittedBox reuses ClipRectLayer', () {
|
|
testFittedBoxWithClipRectLayer();
|
|
});
|
|
|
|
test('RenderFittedBox reuses TransformLayer', () {
|
|
testFittedBoxWithTransformLayer();
|
|
});
|
|
|
|
test('RenderFittedBox switches between ClipRectLayer and TransformLayer, and reuses them', () {
|
|
testFittedBoxWithClipRectLayer();
|
|
|
|
// clip -> transform
|
|
testFittedBoxWithTransformLayer();
|
|
// transform -> clip
|
|
testFittedBoxWithClipRectLayer();
|
|
});
|
|
|
|
test('RenderFittedBox respects clipBehavior', () {
|
|
const BoxConstraints viewport = BoxConstraints(maxHeight: 100.0, maxWidth: 100.0);
|
|
for (final Clip? clip in <Clip?>[null, ...Clip.values]) {
|
|
final TestClipPaintingContext context = TestClipPaintingContext();
|
|
final RenderFittedBox box;
|
|
switch (clip) {
|
|
case Clip.none:
|
|
case Clip.hardEdge:
|
|
case Clip.antiAlias:
|
|
case Clip.antiAliasWithSaveLayer:
|
|
box = RenderFittedBox(child: box200x200, fit: BoxFit.none, clipBehavior: clip!);
|
|
case null:
|
|
box = RenderFittedBox(child: box200x200, fit: BoxFit.none);
|
|
}
|
|
layout(box, constraints: viewport, phase: EnginePhase.composite, onErrors: expectNoFlutterErrors);
|
|
box.paint(context, Offset.zero);
|
|
// By default, clipBehavior should be Clip.none
|
|
expect(context.clipBehavior, equals(clip ?? Clip.none));
|
|
}
|
|
});
|
|
|
|
test('RenderMouseRegion can change properties when detached', () {
|
|
final RenderMouseRegion object = RenderMouseRegion();
|
|
object
|
|
..opaque = false
|
|
..onEnter = (_) {}
|
|
..onExit = (_) {}
|
|
..onHover = (_) {};
|
|
// Passes if no error is thrown
|
|
});
|
|
|
|
test('RenderFractionalTranslation updates its semantics after its translation value is set', () {
|
|
final _TestSemanticsUpdateRenderFractionalTranslation box = _TestSemanticsUpdateRenderFractionalTranslation(
|
|
translation: const Offset(0.5, 0.5),
|
|
);
|
|
layout(box, constraints: BoxConstraints.tight(const Size(200.0, 200.0)));
|
|
expect(box.markNeedsSemanticsUpdateCallCount, 1);
|
|
box.translation = const Offset(0.4, 0.4);
|
|
expect(box.markNeedsSemanticsUpdateCallCount, 2);
|
|
box.translation = const Offset(0.3, 0.3);
|
|
expect(box.markNeedsSemanticsUpdateCallCount, 3);
|
|
});
|
|
|
|
test('RenderFollowerLayer hit test without a leader layer and the showWhenUnlinked is true', () {
|
|
final RenderFollowerLayer follower = RenderFollowerLayer(
|
|
link: LayerLink(),
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
);
|
|
layout(follower, constraints: BoxConstraints.tight(const Size(200.0, 200.0)));
|
|
final BoxHitTestResult hitTestResult = BoxHitTestResult();
|
|
expect(follower.hitTest(hitTestResult, position: Offset.zero), isTrue);
|
|
});
|
|
|
|
test('RenderFollowerLayer hit test without a leader layer and the showWhenUnlinked is false', () {
|
|
final RenderFollowerLayer follower = RenderFollowerLayer(
|
|
link: LayerLink(),
|
|
showWhenUnlinked: false,
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
);
|
|
layout(follower, constraints: BoxConstraints.tight(const Size(200.0, 200.0)));
|
|
final BoxHitTestResult hitTestResult = BoxHitTestResult();
|
|
expect(follower.hitTest(hitTestResult, position: Offset.zero), isFalse);
|
|
});
|
|
|
|
test('RenderFollowerLayer hit test with a leader layer and the showWhenUnlinked is true', () {
|
|
// Creates a layer link with a leader.
|
|
final LayerLink link = LayerLink();
|
|
final LeaderLayer leader = LeaderLayer(link: link);
|
|
leader.attach(Object());
|
|
|
|
final RenderFollowerLayer follower = RenderFollowerLayer(
|
|
link: link,
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
);
|
|
layout(follower, constraints: BoxConstraints.tight(const Size(200.0, 200.0)));
|
|
final BoxHitTestResult hitTestResult = BoxHitTestResult();
|
|
expect(follower.hitTest(hitTestResult, position: Offset.zero), isTrue);
|
|
});
|
|
|
|
test('RenderFollowerLayer hit test with a leader layer and the showWhenUnlinked is false', () {
|
|
// Creates a layer link with a leader.
|
|
final LayerLink link = LayerLink();
|
|
final LeaderLayer leader = LeaderLayer(link: link);
|
|
leader.attach(Object());
|
|
|
|
final RenderFollowerLayer follower = RenderFollowerLayer(
|
|
link: link,
|
|
showWhenUnlinked: false,
|
|
child: RenderSizedBox(const Size(1.0, 1.0)),
|
|
);
|
|
layout(follower, constraints: BoxConstraints.tight(const Size(200.0, 200.0)));
|
|
final BoxHitTestResult hitTestResult = BoxHitTestResult();
|
|
// 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);
|
|
});
|
|
|
|
test('Offstage implements paintsChild correctly', () {
|
|
final RenderConstrainedBox box = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
|
|
final RenderConstrainedBox parent = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
|
|
final RenderOffstage offstage = RenderOffstage(offstage: false, child: box);
|
|
parent.child = offstage;
|
|
|
|
expect(offstage.paintsChild(box), true);
|
|
|
|
offstage.offstage = true;
|
|
|
|
expect(offstage.paintsChild(box), false);
|
|
});
|
|
|
|
test('Opacity implements paintsChild correctly', () {
|
|
final RenderBox box = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
|
|
final RenderOpacity opacity = RenderOpacity(child: box);
|
|
|
|
expect(opacity.paintsChild(box), true);
|
|
|
|
opacity.opacity = 0;
|
|
|
|
expect(opacity.paintsChild(box), false);
|
|
});
|
|
|
|
test('AnimatedOpacity sets paint matrix to zero when alpha == 0', () {
|
|
final RenderBox box = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20));
|
|
final AnimationController opacityAnimation = AnimationController(value: 1, vsync: FakeTickerProvider());
|
|
final RenderAnimatedOpacity opacity = RenderAnimatedOpacity(opacity: opacityAnimation, child: box);
|
|
|
|
// Make it listen to the animation.
|
|
opacity.attach(PipelineOwner());
|
|
|
|
expect(opacity.paintsChild(box), true);
|
|
|
|
opacityAnimation.value = 0;
|
|
|
|
expect(opacity.paintsChild(box), false);
|
|
});
|
|
|
|
test('AnimatedOpacity sets paint matrix to zero when alpha == 0 (sliver)', () {
|
|
final RenderSliver sliver = RenderSliverToBoxAdapter(child: RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 20)));
|
|
final AnimationController opacityAnimation = AnimationController(value: 1, vsync: FakeTickerProvider());
|
|
final RenderSliverAnimatedOpacity opacity = RenderSliverAnimatedOpacity(opacity: opacityAnimation, sliver: sliver);
|
|
|
|
// Make it listen to the animation.
|
|
opacity.attach(PipelineOwner());
|
|
|
|
expect(opacity.paintsChild(sliver), true);
|
|
|
|
opacityAnimation.value = 0;
|
|
|
|
expect(opacity.paintsChild(sliver), false);
|
|
});
|
|
|
|
test('RenderCustomClip extenders respect clipBehavior when asked to describeApproximateClip', () {
|
|
final RenderBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200, height: 200));
|
|
final RenderClipRect renderClipRect = RenderClipRect(clipBehavior: Clip.none, child: child);
|
|
layout(renderClipRect);
|
|
expect(
|
|
renderClipRect.describeApproximatePaintClip(child),
|
|
null,
|
|
);
|
|
renderClipRect.clipBehavior = Clip.hardEdge;
|
|
expect(
|
|
renderClipRect.describeApproximatePaintClip(child),
|
|
Offset.zero & renderClipRect.size,
|
|
);
|
|
renderClipRect.clipBehavior = Clip.antiAlias;
|
|
expect(
|
|
renderClipRect.describeApproximatePaintClip(child),
|
|
Offset.zero & renderClipRect.size,
|
|
);
|
|
renderClipRect.clipBehavior = Clip.antiAliasWithSaveLayer;
|
|
expect(
|
|
renderClipRect.describeApproximatePaintClip(child),
|
|
Offset.zero & renderClipRect.size,
|
|
);
|
|
});
|
|
|
|
// Simulate painting a RenderBox as if 'debugPaintSizeEnabled == true'
|
|
DebugPaintCallback debugPaint(RenderBox renderBox) {
|
|
layout(renderBox);
|
|
pumpFrame(phase: EnginePhase.compositingBits);
|
|
return (PaintingContext context, Offset offset) {
|
|
renderBox.paint(context, offset);
|
|
renderBox.debugPaintSize(context, offset);
|
|
};
|
|
}
|
|
|
|
test('RenderClipPath.debugPaintSize draws a path and a debug text when clipBehavior is not Clip.none', () {
|
|
DebugPaintCallback debugPaintClipRect(Clip clip) {
|
|
final RenderBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200, height: 200));
|
|
final RenderClipPath renderClipPath = RenderClipPath(clipBehavior: clip, child: child);
|
|
return debugPaint(renderClipPath);
|
|
}
|
|
|
|
// RenderClipPath.debugPaintSize draws when clipBehavior is not Clip.none
|
|
expect(debugPaintClipRect(Clip.hardEdge), paintsExactlyCountTimes(#drawPath, 1));
|
|
expect(debugPaintClipRect(Clip.hardEdge), paintsExactlyCountTimes(#drawParagraph, 1));
|
|
|
|
// RenderClipPath.debugPaintSize does not draw when clipBehavior is Clip.none
|
|
// Regression test for https://github.com/flutter/flutter/issues/105969
|
|
expect(debugPaintClipRect(Clip.none), paintsExactlyCountTimes(#drawPath, 0));
|
|
expect(debugPaintClipRect(Clip.none), paintsExactlyCountTimes(#drawParagraph, 0));
|
|
});
|
|
|
|
test('RenderClipRect.debugPaintSize draws a rect and a debug text when clipBehavior is not Clip.none', () {
|
|
DebugPaintCallback debugPaintClipRect(Clip clip) {
|
|
final RenderBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200, height: 200));
|
|
final RenderClipRect renderClipRect = RenderClipRect(clipBehavior: clip, child: child);
|
|
return debugPaint(renderClipRect);
|
|
}
|
|
|
|
// RenderClipRect.debugPaintSize draws when clipBehavior is not Clip.none
|
|
expect(debugPaintClipRect(Clip.hardEdge), paintsExactlyCountTimes(#drawRect, 1));
|
|
expect(debugPaintClipRect(Clip.hardEdge), paintsExactlyCountTimes(#drawParagraph, 1));
|
|
|
|
// RenderClipRect.debugPaintSize does not draw when clipBehavior is Clip.none
|
|
expect(debugPaintClipRect(Clip.none), paintsExactlyCountTimes(#drawRect, 0));
|
|
expect(debugPaintClipRect(Clip.none), paintsExactlyCountTimes(#drawParagraph, 0));
|
|
});
|
|
|
|
test('RenderClipRRect.debugPaintSize draws a rounded rect and a debug text when clipBehavior is not Clip.none', () {
|
|
DebugPaintCallback debugPaintClipRRect(Clip clip) {
|
|
final RenderBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200, height: 200));
|
|
final RenderClipRRect renderClipRRect = RenderClipRRect(clipBehavior: clip, child: child);
|
|
return debugPaint(renderClipRRect);
|
|
}
|
|
|
|
// RenderClipRRect.debugPaintSize draws when clipBehavior is not Clip.none
|
|
expect(debugPaintClipRRect(Clip.hardEdge), paintsExactlyCountTimes(#drawRRect, 1));
|
|
expect(debugPaintClipRRect(Clip.hardEdge), paintsExactlyCountTimes(#drawParagraph, 1));
|
|
|
|
// RenderClipRRect.debugPaintSize does not draw when clipBehavior is Clip.none
|
|
expect(debugPaintClipRRect(Clip.none), paintsExactlyCountTimes(#drawRRect, 0));
|
|
expect(debugPaintClipRRect(Clip.none), paintsExactlyCountTimes(#drawParagraph, 0));
|
|
});
|
|
|
|
test('RenderClipOval.debugPaintSize draws a path and a debug text when clipBehavior is not Clip.none', () {
|
|
DebugPaintCallback debugPaintClipOval(Clip clip) {
|
|
final RenderBox child = RenderConstrainedBox(additionalConstraints: const BoxConstraints.tightFor(width: 200, height: 200));
|
|
final RenderClipOval renderClipOval = RenderClipOval(clipBehavior: clip, child: child);
|
|
return debugPaint(renderClipOval);
|
|
}
|
|
|
|
// RenderClipOval.debugPaintSize draws when clipBehavior is not Clip.none
|
|
expect(debugPaintClipOval(Clip.hardEdge), paintsExactlyCountTimes(#drawPath, 1));
|
|
expect(debugPaintClipOval(Clip.hardEdge), paintsExactlyCountTimes(#drawParagraph, 1));
|
|
|
|
// RenderClipOval.debugPaintSize does not draw when clipBehavior is Clip.none
|
|
expect(debugPaintClipOval(Clip.none), paintsExactlyCountTimes(#drawPath, 0));
|
|
expect(debugPaintClipOval(Clip.none), paintsExactlyCountTimes(#drawParagraph, 0));
|
|
});
|
|
|
|
test('RenderProxyBox behavior can be mixed in along with another base class', () {
|
|
final RenderFancyProxyBox fancyProxyBox = RenderFancyProxyBox(fancy: 6);
|
|
// Box has behavior from its base class:
|
|
expect(fancyProxyBox.fancyMethod(), 36);
|
|
// Box has behavior from RenderProxyBox:
|
|
expect(
|
|
// ignore: invalid_use_of_protected_member
|
|
fancyProxyBox.computeDryLayout(const BoxConstraints(minHeight: 8)),
|
|
const Size(0, 8),
|
|
);
|
|
});
|
|
|
|
test('computeDryLayout constraints are covariant', () {
|
|
final RenderBoxWithTestConstraints box = RenderBoxWithTestConstraints();
|
|
const TestConstraints constraints = TestConstraints(testValue: 6);
|
|
expect(box.computeDryLayout(constraints), const Size.square(6));
|
|
});
|
|
}
|
|
|
|
class _TestRectClipper extends CustomClipper<Rect> {
|
|
@override
|
|
Rect getClip(Size size) {
|
|
return Rect.zero;
|
|
}
|
|
|
|
@override
|
|
Rect getApproximateClipRect(Size size) => getClip(size);
|
|
|
|
@override
|
|
bool shouldReclip(_TestRectClipper oldClipper) => true;
|
|
}
|
|
|
|
class _TestRRectClipper extends CustomClipper<RRect> {
|
|
@override
|
|
RRect getClip(Size size) {
|
|
return RRect.zero;
|
|
}
|
|
|
|
@override
|
|
Rect getApproximateClipRect(Size size) => getClip(size).outerRect;
|
|
|
|
@override
|
|
bool shouldReclip(_TestRRectClipper oldClipper) => true;
|
|
}
|
|
|
|
// Forces two frames and checks that:
|
|
// - a layer is created on the first frame
|
|
// - the layer is reused on the second frame
|
|
void _testLayerReuse<L extends Layer>(RenderBox renderObject) {
|
|
expect(L, isNot(Layer));
|
|
expect(renderObject.debugLayer, null);
|
|
layout(renderObject, phase: EnginePhase.paint, constraints: BoxConstraints.tight(const Size(10, 10)));
|
|
final Layer? layer = renderObject.debugLayer;
|
|
expect(layer, isA<L>());
|
|
expect(layer, isNotNull);
|
|
|
|
// Mark for repaint otherwise pumpFrame is a noop.
|
|
renderObject.markNeedsPaint();
|
|
expect(renderObject.debugNeedsPaint, true);
|
|
pumpFrame(phase: EnginePhase.paint);
|
|
expect(renderObject.debugNeedsPaint, false);
|
|
expect(renderObject.debugLayer, same(layer));
|
|
}
|
|
|
|
class _TestPathClipper extends CustomClipper<Path> {
|
|
@override
|
|
Path getClip(Size size) {
|
|
return Path()
|
|
..addRect(const Rect.fromLTWH(50.0, 50.0, 100.0, 100.0));
|
|
}
|
|
@override
|
|
bool shouldReclip(_TestPathClipper oldClipper) => false;
|
|
}
|
|
|
|
class _TestSemanticsUpdateRenderFractionalTranslation extends RenderFractionalTranslation {
|
|
_TestSemanticsUpdateRenderFractionalTranslation({
|
|
required super.translation,
|
|
});
|
|
|
|
int markNeedsSemanticsUpdateCallCount = 0;
|
|
|
|
@override
|
|
void markNeedsSemanticsUpdate() {
|
|
markNeedsSemanticsUpdateCallCount++;
|
|
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 {}
|
|
|
|
class RenderFancyBox extends RenderBox {
|
|
RenderFancyBox({required this.fancy}) : super();
|
|
|
|
late int fancy;
|
|
|
|
int fancyMethod() {
|
|
return fancy * fancy;
|
|
}
|
|
}
|
|
|
|
class RenderFancyProxyBox extends RenderFancyBox
|
|
with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin<RenderBox> {
|
|
RenderFancyProxyBox({required super.fancy});
|
|
}
|
|
|
|
void expectAssertionError() {
|
|
final FlutterErrorDetails errorDetails = TestRenderingFlutterBinding.instance.takeFlutterErrorDetails()!;
|
|
final bool asserted = errorDetails.toString().contains('Failed assertion');
|
|
if (!asserted) {
|
|
FlutterError.reportError(errorDetails);
|
|
}
|
|
}
|
|
|
|
typedef DebugPaintCallback = void Function(PaintingContext context, Offset offset);
|
|
|
|
class TestConstraints extends BoxConstraints {
|
|
const TestConstraints({
|
|
double extent = 100,
|
|
required this.testValue,
|
|
}) : super(maxWidth: extent, maxHeight: extent);
|
|
|
|
final double testValue;
|
|
}
|
|
|
|
class RenderBoxWithTestConstraints extends RenderProxyBox {
|
|
@override
|
|
Size computeDryLayout(TestConstraints constraints) {
|
|
return constraints.constrain(Size.square(constraints.testValue));
|
|
}
|
|
}
|