mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
LayerLink can temporary allow multiple leaders (#95977)
This commit is contained in:
parent
db5c71f448
commit
e17a1858e5
@ -7,6 +7,7 @@ import 'dart:ui' as ui;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'debug.dart';
|
||||
@ -2110,6 +2111,55 @@ class PhysicalModelLayer extends ContainerLayer {
|
||||
class LayerLink {
|
||||
LeaderLayer? _leader;
|
||||
|
||||
void _registerLeader(LeaderLayer leader) {
|
||||
assert(_leader != leader);
|
||||
assert((){
|
||||
if (_leader != null) {
|
||||
_debugPreviousLeaders ??= <LeaderLayer>{};
|
||||
_debugPreviousLeaders!.add(_leader!);
|
||||
_debugScheduleLeadersCleanUpCheck();
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
_leader = leader;
|
||||
}
|
||||
|
||||
void _unregisterLeader(LeaderLayer leader) {
|
||||
assert(_leader != null);
|
||||
if (_leader == leader) {
|
||||
_leader = null;
|
||||
} else {
|
||||
assert((){
|
||||
_debugPreviousLeaders!.remove(leader);
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores the previous leaders that were replaced by the current [_leader]
|
||||
/// in the current frame.
|
||||
///
|
||||
/// These leaders need to give up their leaderships of this link by the end of
|
||||
/// the current frame.
|
||||
Set<LeaderLayer>? _debugPreviousLeaders;
|
||||
bool _debugLeaderCheckScheduled = false;
|
||||
|
||||
/// Schedules the check as post frame callback to make sure the
|
||||
/// [_debugPreviousLeaders] is empty.
|
||||
void _debugScheduleLeadersCleanUpCheck() {
|
||||
assert(_debugPreviousLeaders != null);
|
||||
assert(() {
|
||||
if (_debugLeaderCheckScheduled)
|
||||
return true;
|
||||
_debugLeaderCheckScheduled = true;
|
||||
SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) {
|
||||
_debugLeaderCheckScheduled = false;
|
||||
assert(_debugPreviousLeaders!.isEmpty);
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
int _connectedFollowers = 0;
|
||||
|
||||
/// Whether a [LeaderLayer] is currently connected to this link.
|
||||
@ -2202,7 +2252,10 @@ class LeaderLayer extends ContainerLayer {
|
||||
if (_link == value) {
|
||||
return;
|
||||
}
|
||||
_link._leader = null;
|
||||
if (attached) {
|
||||
_link._unregisterLeader(this);
|
||||
value._registerLeader(this);
|
||||
}
|
||||
_link = value;
|
||||
}
|
||||
|
||||
@ -2233,16 +2286,14 @@ class LeaderLayer extends ContainerLayer {
|
||||
@override
|
||||
void attach(Object owner) {
|
||||
super.attach(owner);
|
||||
assert(link._leader == null);
|
||||
assert(_debugSetLastOffset(null));
|
||||
link._leader = this;
|
||||
_link._registerLeader(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void detach() {
|
||||
assert(link._leader == this);
|
||||
link._leader = null;
|
||||
assert(_debugSetLastOffset(null));
|
||||
_link._unregisterLeader(this);
|
||||
super.detach();
|
||||
}
|
||||
|
||||
|
@ -164,6 +164,18 @@ void main() {
|
||||
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: window);
|
||||
leaderLayer.attach(view);
|
||||
final LayerLink link2 = LayerLink();
|
||||
leaderLayer.link = link2;
|
||||
// This should not crash.
|
||||
leaderLayer.detach();
|
||||
expect(leaderLayer.link, link2);
|
||||
});
|
||||
|
||||
test('leader layers are always dirty when connected to follower layer', () {
|
||||
final ContainerLayer root = ContainerLayer()..attach(Object());
|
||||
|
||||
|
@ -153,6 +153,76 @@ void main() {
|
||||
expect(renderObject.toStringShort(), contains('DISPOSED'));
|
||||
});
|
||||
|
||||
test('Leader layer can switch to a different render object within one frame', () {
|
||||
List<FlutterErrorDetails?>? caughtErrors;
|
||||
renderer.onErrors = () {
|
||||
caughtErrors = renderer.takeAllFlutterErrorDetails().toList();
|
||||
};
|
||||
|
||||
final LayerLink layerLink = LayerLink();
|
||||
// renderObject1 paints the leader layer first.
|
||||
final LeaderLayerRenderObject renderObject1 = LeaderLayerRenderObject();
|
||||
renderObject1.layerLink = layerLink;
|
||||
renderObject1.attach(renderer.pipelineOwner);
|
||||
final OffsetLayer rootLayer1 = OffsetLayer();
|
||||
rootLayer1.attach(renderObject1);
|
||||
renderObject1.scheduleInitialPaint(rootLayer1);
|
||||
renderObject1.layout(const BoxConstraints.tightForFinite());
|
||||
|
||||
final LeaderLayerRenderObject renderObject2 = LeaderLayerRenderObject();
|
||||
final OffsetLayer rootLayer2 = OffsetLayer();
|
||||
rootLayer2.attach(renderObject2);
|
||||
renderObject2.attach(renderer.pipelineOwner);
|
||||
renderObject2.scheduleInitialPaint(rootLayer2);
|
||||
renderObject2.layout(const BoxConstraints.tightForFinite());
|
||||
renderer.pumpCompleteFrame();
|
||||
|
||||
// Swap the layer link to renderObject2 in the same frame
|
||||
renderObject1.layerLink = null;
|
||||
renderObject1.markNeedsPaint();
|
||||
renderObject2.layerLink = layerLink;
|
||||
renderObject2.markNeedsPaint();
|
||||
renderer.pumpCompleteFrame();
|
||||
|
||||
// Swap the layer link to renderObject1 in the same frame
|
||||
renderObject1.layerLink = layerLink;
|
||||
renderObject1.markNeedsPaint();
|
||||
renderObject2.layerLink = null;
|
||||
renderObject2.markNeedsPaint();
|
||||
renderer.pumpCompleteFrame();
|
||||
|
||||
renderer.onErrors = null;
|
||||
expect(caughtErrors, isNull);
|
||||
});
|
||||
|
||||
test('Leader layer append to two render objects does crash', () {
|
||||
List<FlutterErrorDetails?>? caughtErrors;
|
||||
renderer.onErrors = () {
|
||||
caughtErrors = renderer.takeAllFlutterErrorDetails().toList();
|
||||
};
|
||||
final LayerLink layerLink = LayerLink();
|
||||
// renderObject1 paints the leader layer first.
|
||||
final LeaderLayerRenderObject renderObject1 = LeaderLayerRenderObject();
|
||||
renderObject1.layerLink = layerLink;
|
||||
renderObject1.attach(renderer.pipelineOwner);
|
||||
final OffsetLayer rootLayer1 = OffsetLayer();
|
||||
rootLayer1.attach(renderObject1);
|
||||
renderObject1.scheduleInitialPaint(rootLayer1);
|
||||
renderObject1.layout(const BoxConstraints.tightForFinite());
|
||||
|
||||
final LeaderLayerRenderObject renderObject2 = LeaderLayerRenderObject();
|
||||
renderObject2.layerLink = layerLink;
|
||||
final OffsetLayer rootLayer2 = OffsetLayer();
|
||||
rootLayer2.attach(renderObject2);
|
||||
renderObject2.attach(renderer.pipelineOwner);
|
||||
renderObject2.scheduleInitialPaint(rootLayer2);
|
||||
renderObject2.layout(const BoxConstraints.tightForFinite());
|
||||
renderer.pumpCompleteFrame();
|
||||
|
||||
renderer.onErrors = null;
|
||||
expect(caughtErrors!.isNotEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('RenderObject.dispose null the layer on repaint boundaries', () {
|
||||
final TestRenderObject renderObject = TestRenderObject(allowPaintBounds: true);
|
||||
// Force a layer to get set.
|
||||
@ -255,6 +325,39 @@ class TestRenderObject extends RenderObject {
|
||||
}
|
||||
}
|
||||
|
||||
class LeaderLayerRenderObject extends RenderObject {
|
||||
LeaderLayerRenderObject();
|
||||
|
||||
LayerLink? layerLink;
|
||||
|
||||
@override
|
||||
bool isRepaintBoundary = true;
|
||||
|
||||
@override
|
||||
void debugAssertDoesMeetConstraints() { }
|
||||
|
||||
@override
|
||||
Rect get paintBounds {
|
||||
return Rect.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (layerLink != null) {
|
||||
context.pushLayer(LeaderLayer(link: layerLink!), super.paint, offset);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() { }
|
||||
|
||||
@override
|
||||
void performResize() { }
|
||||
|
||||
@override
|
||||
Rect get semanticBounds => const Rect.fromLTWH(0.0, 0.0, 10.0, 20.0);
|
||||
}
|
||||
|
||||
class TestThrowingRenderObject extends RenderObject {
|
||||
@override
|
||||
void performLayout() {
|
||||
|
@ -82,6 +82,35 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
|
||||
|
||||
EnginePhase phase = EnginePhase.composite;
|
||||
|
||||
/// Pumps a frame and runs its entire life cycle.
|
||||
///
|
||||
/// This method runs all of the [SchedulerPhase]s in a frame, this is useful
|
||||
/// to test [SchedulerPhase.postFrameCallbacks].
|
||||
void pumpCompleteFrame() {
|
||||
final FlutterExceptionHandler? oldErrorHandler = FlutterError.onError;
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
_errors.add(details);
|
||||
};
|
||||
try {
|
||||
renderer.handleBeginFrame(null);
|
||||
renderer.handleDrawFrame();
|
||||
} finally {
|
||||
FlutterError.onError = oldErrorHandler;
|
||||
if (_errors.isNotEmpty) {
|
||||
if (onErrors != null) {
|
||||
onErrors!();
|
||||
if (_errors.isNotEmpty) {
|
||||
_errors.forEach(FlutterError.dumpErrorToConsole);
|
||||
fail('There are more errors than the test inspected using TestRenderingFlutterBinding.takeFlutterErrorDetails.');
|
||||
}
|
||||
} else {
|
||||
_errors.forEach(FlutterError.dumpErrorToConsole);
|
||||
fail('Caught error while rendering frame. See preceding logs for details.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void drawFrame() {
|
||||
assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');
|
||||
|
Loading…
Reference in New Issue
Block a user