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/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:vector_math/vector_math_64.dart';
|
import 'package:vector_math/vector_math_64.dart';
|
||||||
|
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
@ -2110,6 +2111,55 @@ class PhysicalModelLayer extends ContainerLayer {
|
|||||||
class LayerLink {
|
class LayerLink {
|
||||||
LeaderLayer? _leader;
|
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;
|
int _connectedFollowers = 0;
|
||||||
|
|
||||||
/// Whether a [LeaderLayer] is currently connected to this link.
|
/// Whether a [LeaderLayer] is currently connected to this link.
|
||||||
@ -2202,7 +2252,10 @@ class LeaderLayer extends ContainerLayer {
|
|||||||
if (_link == value) {
|
if (_link == value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_link._leader = null;
|
if (attached) {
|
||||||
|
_link._unregisterLeader(this);
|
||||||
|
value._registerLeader(this);
|
||||||
|
}
|
||||||
_link = value;
|
_link = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2233,16 +2286,14 @@ class LeaderLayer extends ContainerLayer {
|
|||||||
@override
|
@override
|
||||||
void attach(Object owner) {
|
void attach(Object owner) {
|
||||||
super.attach(owner);
|
super.attach(owner);
|
||||||
assert(link._leader == null);
|
|
||||||
assert(_debugSetLastOffset(null));
|
assert(_debugSetLastOffset(null));
|
||||||
link._leader = this;
|
_link._registerLeader(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void detach() {
|
void detach() {
|
||||||
assert(link._leader == this);
|
|
||||||
link._leader = null;
|
|
||||||
assert(_debugSetLastOffset(null));
|
assert(_debugSetLastOffset(null));
|
||||||
|
_link._unregisterLeader(this);
|
||||||
super.detach();
|
super.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,6 +164,18 @@ void main() {
|
|||||||
expect(followerLayer.debugSubtreeNeedsAddToScene, true);
|
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', () {
|
test('leader layers are always dirty when connected to follower layer', () {
|
||||||
final ContainerLayer root = ContainerLayer()..attach(Object());
|
final ContainerLayer root = ContainerLayer()..attach(Object());
|
||||||
|
|
||||||
|
@ -153,6 +153,76 @@ void main() {
|
|||||||
expect(renderObject.toStringShort(), contains('DISPOSED'));
|
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', () {
|
test('RenderObject.dispose null the layer on repaint boundaries', () {
|
||||||
final TestRenderObject renderObject = TestRenderObject(allowPaintBounds: true);
|
final TestRenderObject renderObject = TestRenderObject(allowPaintBounds: true);
|
||||||
// Force a layer to get set.
|
// 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 {
|
class TestThrowingRenderObject extends RenderObject {
|
||||||
@override
|
@override
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
|
@ -82,6 +82,35 @@ class TestRenderingFlutterBinding extends BindingBase with SchedulerBinding, Ser
|
|||||||
|
|
||||||
EnginePhase phase = EnginePhase.composite;
|
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
|
@override
|
||||||
void drawFrame() {
|
void drawFrame() {
|
||||||
assert(phase != EnginePhase.build, 'rendering_tester does not support testing the build phase; use flutter_test instead');
|
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