Revert "Add CompositedTransformFollower.{followerAnchor, leaderAnchor} for custom anchoring (#64930)" (#65871)

This reverts commit 2fbb529d2b.
This commit is contained in:
LongCatIsLooong 2020-09-15 11:28:52 -07:00 committed by GitHub
parent 7948a7863b
commit 8eadbb3137
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 97 additions and 314 deletions

View file

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:collection';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
@ -2027,14 +2028,6 @@ class LayerLink {
LeaderLayer? get leader => _leader;
LeaderLayer? _leader;
/// The total size of [leader]'s contents.
///
/// Generally this should be set by the [RenderObject] that paints on the
/// registered [leader] layer (for instance a [RenderLeaderLayer] that shares
/// this link with its followers). This size may be outdated before and during
/// layout.
Size? leaderSize;
@override
String toString() => '${describeIdentity(this)}(${ _leader != null ? "<linked>" : "<dangling>" })';
}
@ -2273,91 +2266,59 @@ class FollowerLayer extends ContainerLayer {
/// treated as the child of the second, and so forth. The first layer in the
/// list won't have [applyTransform] called on it. The first layer may be
/// null.
static Matrix4 _collectTransformForLayerChain(List<ContainerLayer?> layers) {
Matrix4 _collectTransformForLayerChain(List<ContainerLayer?> layers) {
// Initialize our result matrix.
final Matrix4 result = Matrix4.identity();
// Apply each layer to the matrix in turn, starting from the last layer,
// and providing the previous layer as the child.
for (int index = layers.length - 1; index > 0; index -= 1)
layers[index]?.applyTransform(layers[index - 1], result);
layers[index]!.applyTransform(layers[index - 1], result);
return result;
}
/// Find the common ancestor of two layers [a] and [b] by searching towards
/// the root of the tree, and append each ancestor of [a] or [b] visited along
/// the path to [ancestorsA] and [ancestorsB] respectively.
///
/// Returns null if [a] [b] do not share a common ancestor, in which case the
/// results in [ancestorsA] and [ancestorsB] are undefined.
static Layer? _pathsToCommonAncestor(
Layer? a, Layer? b,
List<ContainerLayer?> ancestorsA, List<ContainerLayer?> ancestorsB,
) {
// No common ancestor found.
if (a == null || b == null)
return null;
if (identical(a, b))
return a;
if (a.depth < b.depth) {
ancestorsB.add(b.parent);
return _pathsToCommonAncestor(a, b.parent, ancestorsA, ancestorsB);
} else if (a.depth > b.depth){
ancestorsA.add(a.parent);
return _pathsToCommonAncestor(a.parent, b, ancestorsA, ancestorsB);
}
ancestorsA.add(a.parent);
ancestorsB.add(b.parent);
return _pathsToCommonAncestor(a.parent, b.parent, ancestorsA, ancestorsB);
}
/// Populate [_lastTransform] given the current state of the tree.
void _establishTransform() {
assert(link != null);
_lastTransform = null;
final LeaderLayer? leader = link.leader;
// Check to see if we are linked.
if (leader == null)
if (link.leader == null)
return;
// If we're linked, check the link is valid.
assert(
leader.owner == owner,
'Linked LeaderLayer anchor is not in the same layer tree as the FollowerLayer.',
);
assert(
leader._lastOffset != null,
'LeaderLayer anchor must come before FollowerLayer in paint order, but the reverse was true.',
);
// Stores [leader, ..., commonAncestor] after calling _pathsToCommonAncestor.
final List<ContainerLayer?> forwardLayers = <ContainerLayer>[leader];
// Stores [this (follower), ..., commonAncestor] after calling
// _pathsToCommonAncestor.
final List<ContainerLayer?> inverseLayers = <ContainerLayer>[this];
final Layer? ancestor = _pathsToCommonAncestor(
leader, this,
forwardLayers, inverseLayers,
);
assert(ancestor != null);
assert(link.leader!.owner == owner, 'Linked LeaderLayer anchor is not in the same layer tree as the FollowerLayer.');
assert(link.leader!._lastOffset != null, 'LeaderLayer anchor must come before FollowerLayer in paint order, but the reverse was true.');
// Collect all our ancestors into a Set so we can recognize them.
final Set<Layer> ancestors = HashSet<Layer>();
Layer? ancestor = parent;
while (ancestor != null) {
ancestors.add(ancestor);
ancestor = ancestor.parent;
}
// Collect all the layers from a hypothetical child (null) of the target
// layer up to the common ancestor layer.
ContainerLayer? layer = link.leader;
final List<ContainerLayer?> forwardLayers = <ContainerLayer?>[null, layer];
do {
layer = layer!.parent;
forwardLayers.add(layer);
} while (!ancestors.contains(layer)); // ignore: iterable_contains_unrelated_type
ancestor = layer;
// Collect all the layers from this layer up to the common ancestor layer.
layer = this;
final List<ContainerLayer> inverseLayers = <ContainerLayer>[layer];
do {
layer = layer!.parent;
inverseLayers.add(layer!);
} while (layer != ancestor);
// Establish the forward and backward matrices given these lists of layers.
final Matrix4 forwardTransform = _collectTransformForLayerChain(forwardLayers);
// Further transforms the coordinate system to a hypothetical child (null)
// of the leader layer, to account for the leader's additional paint offset
// and layer offset (LeaderLayer._lastOffset).
leader.applyTransform(null, forwardTransform);
forwardTransform.translate(linkedOffset!.dx, linkedOffset!.dy);
final Matrix4 inverseTransform = _collectTransformForLayerChain(inverseLayers);
if (inverseTransform.invert() == 0.0) {
// We are in a degenerate transform, so there's not much we can do.
return;
}
// Combine the matrices and store the result.
inverseTransform.multiply(forwardTransform);
inverseTransform.translate(linkedOffset!.dx, linkedOffset!.dy);
_lastTransform = inverseTransform;
_inverseDirty = true;
}

View file

@ -4826,37 +4826,28 @@ class RenderLeaderLayer extends RenderProxyBox {
required LayerLink link,
RenderBox? child,
}) : assert(link != null),
_link = link,
super(child);
super(child) {
this.link = link;
}
/// The link object that connects this [RenderLeaderLayer] with one or more
/// [RenderFollowerLayer]s.
///
/// This property must not be null. The object must not be associated with
/// another [RenderLeaderLayer] that is also being painted.
LayerLink get link => _link;
LayerLink _link;
LayerLink get link => _link!;
LayerLink? _link;
set link(LayerLink value) {
assert(value != null);
if (_link == value)
return;
_link.leaderSize = null;
_link = value;
if (hasSize) {
_link.leaderSize = size;
}
markNeedsPaint();
}
@override
bool get alwaysNeedsCompositing => true;
@override
void performLayout() {
super.performLayout();
link.leaderSize = size;
}
@override
void paint(PaintingContext context, Offset offset) {
if (layer == null) {
@ -4899,8 +4890,6 @@ class RenderFollowerLayer extends RenderProxyBox {
required LayerLink link,
bool showWhenUnlinked = true,
Offset offset = Offset.zero,
Alignment leaderAnchor = Alignment.topLeft,
Alignment followerAnchor = Alignment.topLeft,
RenderBox? child,
}) : assert(link != null),
assert(showWhenUnlinked != null),
@ -4908,8 +4897,6 @@ class RenderFollowerLayer extends RenderProxyBox {
_link = link,
_showWhenUnlinked = showWhenUnlinked,
_offset = offset,
_leaderAnchor = leaderAnchor,
_followerAnchor = followerAnchor,
super(child);
/// The link object that connects this [RenderFollowerLayer] with a
@ -4955,46 +4942,6 @@ class RenderFollowerLayer extends RenderProxyBox {
markNeedsPaint();
}
/// The anchor point on the linked [RenderLeaderLayer] that [followerAnchor]
/// will line up with.
///
/// {@template flutter.rendering.followerLayer.anchor}
/// For example, when [leaderAnchor] and [followerAnchor] are both
/// [Alignment.topLeft], this [RenderFollowerLayer] will be top left aligned
/// with the linked [RenderLeaderLayer]. When [leaderAnchor] is
/// [Alignment.bottomLeft] and [followerAnchor] is [Alignment.topLeft], this
/// [RenderFollowerLayer] will be left aligned with the linked
/// [RenderLeaderLayer], and its top edge will line up with the
/// [RenderLeaderLayer]'s bottom edge.
/// {@endtemplate}
///
/// Defaults to [Alignment.topLeft].
Alignment get leaderAnchor => _leaderAnchor;
Alignment _leaderAnchor;
set leaderAnchor(Alignment value) {
assert(value != null);
if (_leaderAnchor == value)
return;
_leaderAnchor = value;
markNeedsPaint();
}
/// The anchor point on this [RenderFollowerLayer] that will line up with
/// [followerAnchor] on the linked [RenderLeaderLayer].
///
/// {@macro flutter.rendering.followerLayer.anchor}
///
/// Defaults to [Alignment.topLeft].
Alignment get followerAnchor => _followerAnchor;
Alignment _followerAnchor;
set followerAnchor(Alignment value) {
assert(value != null);
if (_followerAnchor == value)
return;
_followerAnchor = value;
markNeedsPaint();
}
@override
void detach() {
layer = null;
@ -5043,29 +4990,19 @@ class RenderFollowerLayer extends RenderProxyBox {
@override
void paint(PaintingContext context, Offset offset) {
final Size? leaderSize = link.leaderSize;
assert(
link.leaderSize != null || (link.leader == null || leaderAnchor == Alignment.topLeft),
'$link: layer is linked to ${link.leader} but a valid leaderSize is not set. '
'leaderSize is required when leaderAnchor is not Alignment.topLeft'
'(current value is $leaderAnchor).',
);
final Offset effectiveLinkedOffset = leaderSize == null
? this.offset
: leaderAnchor.alongSize(leaderSize) - followerAnchor.alongSize(size) + this.offset;
assert(showWhenUnlinked != null);
if (layer == null) {
layer = FollowerLayer(
link: link,
showWhenUnlinked: showWhenUnlinked,
linkedOffset: effectiveLinkedOffset,
linkedOffset: this.offset,
unlinkedOffset: offset,
);
} else {
layer
?..link = link
layer!
..link = link
..showWhenUnlinked = showWhenUnlinked
..linkedOffset = effectiveLinkedOffset
..linkedOffset = this.offset
..unlinkedOffset = offset;
}
context.pushLayer(

View file

@ -1327,11 +1327,9 @@ class CompositedTransformTarget extends SingleChildRenderObjectWidget {
///
/// When this widget is composited during the compositing phase (which comes
/// after the paint phase, as described in [WidgetsBinding.drawFrame]), it
/// applies a transformation that brings [targetAnchor] of the linked
/// [CompositedTransformTarget] and [followerAnchor] of this widget together.
/// The two anchor points will have the same global coordinates, unless [offset]
/// is not [Offset.zero], in which case [followerAnchor] will be offset by
/// [offset] in the linked [CompositedTransformTarget]'s coordinate space.
/// applies a transformation that causes it to provide its child with a
/// coordinate space that matches that of the linked [CompositedTransformTarget]
/// widget, offset by [offset].
///
/// The [LayerLink] object used as the [link] must be the same object as that
/// provided to the matching [CompositedTransformTarget].
@ -1363,14 +1361,10 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget {
required this.link,
this.showWhenUnlinked = true,
this.offset = Offset.zero,
this.targetAnchor = Alignment.topLeft,
this.followerAnchor = Alignment.topLeft,
Widget? child,
}) : assert(link != null),
assert(showWhenUnlinked != null),
assert(offset != null),
assert(targetAnchor != null),
assert(followerAnchor != null),
super(key: key, child: child);
/// The link object that connects this [CompositedTransformFollower] with a
@ -1390,33 +1384,8 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget {
/// hidden.
final bool showWhenUnlinked;
/// The anchor point on the linked [CompositedTransformTarget] that
/// [followerAnchor] will line up with.
///
/// {@template flutter.widgets.followerLayer.anchor}
/// For example, when [targetAnchor] and [followerAnchor] are both
/// [Alignment.topLeft], this widget will be top left aligned with the linked
/// [CompositedTransformTarget]. When [targetAnchor] is
/// [Alignment.bottomLeft] and [followerAnchor] is [Alignment.topLeft], this
/// widget will be left aligned with the linked [CompositedTransformTarget],
/// and its top edge will line up with the [CompositedTransformTarget]'s
/// bottom edge.
/// {@endtemplate}
///
/// Defaults to [Alignment.topLeft].
final Alignment targetAnchor;
/// The anchor point on this widget that will line up with [followerAnchor] on
/// the linked [CompositedTransformTarget].
///
/// {@macro flutter.widgets.followerLayer.anchor}
///
/// Defaults to [Alignment.topLeft].
final Alignment followerAnchor;
/// The additional offset to apply to the [targetAnchor] of the linked
/// [CompositedTransformTarget] to obtain this widget's [followerAnchor]
/// position.
/// The offset to apply to the origin of the linked
/// [CompositedTransformTarget] to obtain this widget's origin.
final Offset offset;
@override
@ -1425,8 +1394,6 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget {
link: link,
showWhenUnlinked: showWhenUnlinked,
offset: offset,
leaderAnchor: targetAnchor,
followerAnchor: followerAnchor,
);
}
@ -1435,9 +1402,7 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget {
renderObject
..link = link
..showWhenUnlinked = showWhenUnlinked
..offset = offset
..leaderAnchor = targetAnchor
..followerAnchor = followerAnchor;
..offset = offset;
}
}

View file

@ -9,12 +9,11 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
void main() {
final LayerLink link = LayerLink();
group('Composited transforms - only offsets', () {
testWidgets('Composited transforms - only offsets', (WidgetTester tester) async {
final LayerLink link = LayerLink();
final GlobalKey key = GlobalKey();
Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) {
return Directionality(
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
@ -31,41 +30,23 @@ void main() {
top: 343.0,
child: CompositedTransformFollower(
link: link,
targetAnchor: targetAlignment,
followerAnchor: followerAlignment,
child: Container(key: key, height: 20.0, width: 20.0),
child: Container(key: key, height: 10.0, width: 10.0),
),
),
],
),
);
}
testWidgets('topLeft', (WidgetTester tester) async {
await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft));
final RenderBox box = key.currentContext.findRenderObject() as RenderBox;
expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0));
});
testWidgets('center', (WidgetTester tester) async {
await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center));
final RenderBox box = key.currentContext.findRenderObject() as RenderBox;
expect(box.localToGlobal(Offset.zero), const Offset(118.0, 451.0));
});
testWidgets('bottomRight - topRight', (WidgetTester tester) async {
await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight));
final RenderBox box = key.currentContext.findRenderObject() as RenderBox;
expect(box.localToGlobal(Offset.zero), const Offset(113.0, 466.0));
});
),
);
final RenderBox box = key.currentContext.findRenderObject() as RenderBox;
expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0));
});
group('Composited transforms - with rotations', () {
testWidgets('Composited transforms - with rotations', (WidgetTester tester) async {
final LayerLink link = LayerLink();
final GlobalKey key1 = GlobalKey();
final GlobalKey key2 = GlobalKey();
Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) {
return Directionality(
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
@ -76,7 +57,7 @@ void main() {
angle: 1.0, // radians
child: CompositedTransformTarget(
link: link,
child: Container(key: key1, width: 80.0, height: 10.0),
child: Container(key: key1, height: 10.0, width: 10.0),
),
),
),
@ -87,50 +68,28 @@ void main() {
angle: -0.3, // radians
child: CompositedTransformFollower(
link: link,
targetAnchor: targetAlignment,
followerAnchor: followerAlignment,
child: Container(key: key2, width: 40.0, height: 20.0),
child: Container(key: key2, height: 10.0, width: 10.0),
),
),
),
],
),
);
}
testWidgets('topLeft', (WidgetTester tester) async {
await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft));
final RenderBox box1 = key1.currentContext.findRenderObject() as RenderBox;
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
final Offset position1 = box1.localToGlobal(Offset.zero);
final Offset position2 = box2.localToGlobal(Offset.zero);
expect(position1, offsetMoreOrLessEquals(position2));
});
testWidgets('center', (WidgetTester tester) async {
await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center));
final RenderBox box1 = key1.currentContext.findRenderObject() as RenderBox;
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
final Offset position1 = box1.localToGlobal(const Offset(40, 5));
final Offset position2 = box2.localToGlobal(const Offset(20, 10));
expect(position1, offsetMoreOrLessEquals(position2));
});
testWidgets('bottomRight - topRight', (WidgetTester tester) async {
await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight));
final RenderBox box1 = key1.currentContext.findRenderObject() as RenderBox;
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
final Offset position1 = box1.localToGlobal(const Offset(80, 10));
final Offset position2 = box2.localToGlobal(const Offset(40, 0));
expect(position1, offsetMoreOrLessEquals(position2));
});
),
);
final RenderBox box1 = key1.currentContext.findRenderObject() as RenderBox;
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
final Offset position1 = box1.localToGlobal(Offset.zero);
final Offset position2 = box2.localToGlobal(Offset.zero);
expect(position1.dx, moreOrLessEquals(position2.dx));
expect(position1.dy, moreOrLessEquals(position2.dy));
});
group('Composited transforms - nested', () {
testWidgets('Composited transforms - nested', (WidgetTester tester) async {
final LayerLink link = LayerLink();
final GlobalKey key1 = GlobalKey();
final GlobalKey key2 = GlobalKey();
Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) {
return Directionality(
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
@ -141,7 +100,7 @@ void main() {
angle: 1.0, // radians
child: CompositedTransformTarget(
link: link,
child: Container(key: key1, width: 80.0, height: 10.0),
child: Container(key: key1, height: 10.0, width: 10.0),
),
),
),
@ -160,9 +119,7 @@ void main() {
padding: const EdgeInsets.all(20.0),
child: CompositedTransformFollower(
link: link,
targetAnchor: targetAlignment,
followerAnchor: followerAlignment,
child: Container(key: key2, width: 40.0, height: 20.0),
child: Container(key: key2, height: 10.0, width: 10.0),
),
),
),
@ -172,45 +129,24 @@ void main() {
),
],
),
);
}
testWidgets('topLeft', (WidgetTester tester) async {
await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft));
final RenderBox box1 = key1.currentContext.findRenderObject() as RenderBox;
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
final Offset position1 = box1.localToGlobal(Offset.zero);
final Offset position2 = box2.localToGlobal(Offset.zero);
expect(position1, offsetMoreOrLessEquals(position2));
});
testWidgets('center', (WidgetTester tester) async {
await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center));
final RenderBox box1 = key1.currentContext.findRenderObject() as RenderBox;
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
final Offset position1 = box1.localToGlobal(Alignment.center.alongSize(const Size(80, 10)));
final Offset position2 = box2.localToGlobal(Alignment.center.alongSize(const Size(40, 20)));
expect(position1, offsetMoreOrLessEquals(position2));
});
testWidgets('bottomRight - topRight', (WidgetTester tester) async {
await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight));
final RenderBox box1 = key1.currentContext.findRenderObject() as RenderBox;
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
final Offset position1 = box1.localToGlobal(Alignment.bottomRight.alongSize(const Size(80, 10)));
final Offset position2 = box2.localToGlobal(Alignment.topRight.alongSize(const Size(40, 20)));
expect(position1, offsetMoreOrLessEquals(position2));
});
),
);
final RenderBox box1 = key1.currentContext.findRenderObject() as RenderBox;
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
final Offset position1 = box1.localToGlobal(Offset.zero);
final Offset position2 = box2.localToGlobal(Offset.zero);
expect(position1.dx, moreOrLessEquals(position2.dx));
expect(position1.dy, moreOrLessEquals(position2.dy));
});
group('Composited transforms - hit testing', () {
testWidgets('Composited transforms - hit testing', (WidgetTester tester) async {
final LayerLink link = LayerLink();
final GlobalKey key1 = GlobalKey();
final GlobalKey key2 = GlobalKey();
final GlobalKey key3 = GlobalKey();
bool tapped = false;
Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) {
return Directionality(
bool _tapped = false;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
@ -227,34 +163,18 @@ void main() {
child: GestureDetector(
key: key2,
behavior: HitTestBehavior.opaque,
onTap: () { tapped = true; },
child: Container(key: key3, height: 2.0, width: 2.0),
onTap: () { _tapped = true; },
child: Container(key: key3, height: 10.0, width: 10.0),
),
),
],
),
);
}
const List<Alignment> alignments = <Alignment>[
Alignment.topLeft, Alignment.topRight,
Alignment.center,
Alignment.bottomLeft, Alignment.bottomRight,
];
setUp(() { tapped = false; });
for (final Alignment targetAlignment in alignments) {
for (final Alignment followerAlignment in alignments) {
testWidgets('$targetAlignment - $followerAlignment', (WidgetTester tester) async{
await tester.pumpWidget(build(targetAlignment: targetAlignment, followerAlignment: followerAlignment));
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
expect(box2.size, const Size(2.0, 2.0));
expect(tapped, isFalse);
await tester.tap(find.byKey(key3));
expect(tapped, isTrue);
});
}
}
),
);
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
expect(box2.size, const Size(10.0, 10.0));
expect(_tapped, isFalse);
await tester.tap(find.byKey(key1));
expect(_tapped, isTrue);
});
}