Unverified Commit 73137401 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Reland "Add CompositedTransformFollower.{followerAnchor, leaderAnchor} for...

Reland "Add CompositedTransformFollower.{followerAnchor, leaderAnchor} for custom anchoring (#64930)" (#65871) (#65884)
parent 4513e96a
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:collection';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -2027,6 +2026,14 @@ class LayerLink { ...@@ -2027,6 +2026,14 @@ class LayerLink {
LeaderLayer? get leader => _leader; LeaderLayer? get leader => _leader;
LeaderLayer? _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 @override
String toString() => '${describeIdentity(this)}(${ _leader != null ? "<linked>" : "<dangling>" })'; String toString() => '${describeIdentity(this)}(${ _leader != null ? "<linked>" : "<dangling>" })';
} }
...@@ -2265,59 +2272,91 @@ class FollowerLayer extends ContainerLayer { ...@@ -2265,59 +2272,91 @@ class FollowerLayer extends ContainerLayer {
/// treated as the child of the second, and so forth. The first layer in the /// 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 /// list won't have [applyTransform] called on it. The first layer may be
/// null. /// null.
Matrix4 _collectTransformForLayerChain(List<ContainerLayer?> layers) { static Matrix4 _collectTransformForLayerChain(List<ContainerLayer?> layers) {
// Initialize our result matrix. // Initialize our result matrix.
final Matrix4 result = Matrix4.identity(); final Matrix4 result = Matrix4.identity();
// Apply each layer to the matrix in turn, starting from the last layer, // Apply each layer to the matrix in turn, starting from the last layer,
// and providing the previous layer as the child. // and providing the previous layer as the child.
for (int index = layers.length - 1; index > 0; index -= 1) 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; 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. /// Populate [_lastTransform] given the current state of the tree.
void _establishTransform() { void _establishTransform() {
assert(link != null); assert(link != null);
_lastTransform = null; _lastTransform = null;
final LeaderLayer? leader = link.leader;
// Check to see if we are linked. // Check to see if we are linked.
if (link.leader == null) if (leader == null)
return; return;
// If we're linked, check the link is valid. // If we're linked, check the link is valid.
assert(link.leader!.owner == owner, 'Linked LeaderLayer anchor is not in the same layer tree as the FollowerLayer.'); assert(
assert(link.leader!._lastOffset != null, 'LeaderLayer anchor must come before FollowerLayer in paint order, but the reverse was true.'); leader.owner == owner,
// Collect all our ancestors into a Set so we can recognize them. 'Linked LeaderLayer anchor is not in the same layer tree as the FollowerLayer.',
final Set<Layer> ancestors = HashSet<Layer>(); );
Layer? ancestor = parent; assert(
while (ancestor != null) { leader._lastOffset != null,
ancestors.add(ancestor); 'LeaderLayer anchor must come before FollowerLayer in paint order, but the reverse was true.',
ancestor = ancestor.parent; );
}
// Collect all the layers from a hypothetical child (null) of the target // Stores [leader, ..., commonAncestor] after calling _pathsToCommonAncestor.
// layer up to the common ancestor layer. final List<ContainerLayer?> forwardLayers = <ContainerLayer>[leader];
ContainerLayer? layer = link.leader; // Stores [this (follower), ..., commonAncestor] after calling
final List<ContainerLayer?> forwardLayers = <ContainerLayer?>[null, layer]; // _pathsToCommonAncestor.
do { final List<ContainerLayer?> inverseLayers = <ContainerLayer>[this];
layer = layer!.parent;
forwardLayers.add(layer); final Layer? ancestor = _pathsToCommonAncestor(
} while (!ancestors.contains(layer)); // ignore: iterable_contains_unrelated_type leader, this,
ancestor = layer; forwardLayers, inverseLayers,
// Collect all the layers from this layer up to the common ancestor layer. );
layer = this; assert(ancestor != null);
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); 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); final Matrix4 inverseTransform = _collectTransformForLayerChain(inverseLayers);
if (inverseTransform.invert() == 0.0) { if (inverseTransform.invert() == 0.0) {
// We are in a degenerate transform, so there's not much we can do. // We are in a degenerate transform, so there's not much we can do.
return; return;
} }
// Combine the matrices and store the result. // Combine the matrices and store the result.
inverseTransform.multiply(forwardTransform); inverseTransform.multiply(forwardTransform);
inverseTransform.translate(linkedOffset!.dx, linkedOffset!.dy);
_lastTransform = inverseTransform; _lastTransform = inverseTransform;
_inverseDirty = true; _inverseDirty = true;
} }
......
...@@ -4848,28 +4848,43 @@ class RenderLeaderLayer extends RenderProxyBox { ...@@ -4848,28 +4848,43 @@ class RenderLeaderLayer extends RenderProxyBox {
required LayerLink link, required LayerLink link,
RenderBox? child, RenderBox? child,
}) : assert(link != null), }) : assert(link != null),
super(child) { _link = link,
this.link = link; super(child);
}
/// The link object that connects this [RenderLeaderLayer] with one or more /// The link object that connects this [RenderLeaderLayer] with one or more
/// [RenderFollowerLayer]s. /// [RenderFollowerLayer]s.
/// ///
/// This property must not be null. The object must not be associated with /// This property must not be null. The object must not be associated with
/// another [RenderLeaderLayer] that is also being painted. /// another [RenderLeaderLayer] that is also being painted.
LayerLink get link => _link!; LayerLink get link => _link;
LayerLink? _link; LayerLink _link;
set link(LayerLink value) { set link(LayerLink value) {
assert(value != null); assert(value != null);
if (_link == value) if (_link == value)
return; return;
_link.leaderSize = null;
_link = value; _link = value;
if (_previousLayoutSize != null) {
_link.leaderSize = _previousLayoutSize;
}
markNeedsPaint(); markNeedsPaint();
} }
@override @override
bool get alwaysNeedsCompositing => true; bool get alwaysNeedsCompositing => true;
// The latest size of this [RenderBox], computed during the previous layout
// pass. It should always be equal to [size], but can be accessed even when
// [debugDoingThisResize] and [debugDoingThisLayout] are false.
Size? _previousLayoutSize;
@override
void performLayout() {
super.performLayout();
_previousLayoutSize = size;
link.leaderSize = size;
}
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (layer == null) { if (layer == null) {
...@@ -4912,6 +4927,8 @@ class RenderFollowerLayer extends RenderProxyBox { ...@@ -4912,6 +4927,8 @@ class RenderFollowerLayer extends RenderProxyBox {
required LayerLink link, required LayerLink link,
bool showWhenUnlinked = true, bool showWhenUnlinked = true,
Offset offset = Offset.zero, Offset offset = Offset.zero,
Alignment leaderAnchor = Alignment.topLeft,
Alignment followerAnchor = Alignment.topLeft,
RenderBox? child, RenderBox? child,
}) : assert(link != null), }) : assert(link != null),
assert(showWhenUnlinked != null), assert(showWhenUnlinked != null),
...@@ -4919,6 +4936,8 @@ class RenderFollowerLayer extends RenderProxyBox { ...@@ -4919,6 +4936,8 @@ class RenderFollowerLayer extends RenderProxyBox {
_link = link, _link = link,
_showWhenUnlinked = showWhenUnlinked, _showWhenUnlinked = showWhenUnlinked,
_offset = offset, _offset = offset,
_leaderAnchor = leaderAnchor,
_followerAnchor = followerAnchor,
super(child); super(child);
/// The link object that connects this [RenderFollowerLayer] with a /// The link object that connects this [RenderFollowerLayer] with a
...@@ -4964,6 +4983,46 @@ class RenderFollowerLayer extends RenderProxyBox { ...@@ -4964,6 +4983,46 @@ class RenderFollowerLayer extends RenderProxyBox {
markNeedsPaint(); 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 @override
void detach() { void detach() {
layer = null; layer = null;
...@@ -5012,19 +5071,29 @@ class RenderFollowerLayer extends RenderProxyBox { ...@@ -5012,19 +5071,29 @@ class RenderFollowerLayer extends RenderProxyBox {
@override @override
void paint(PaintingContext context, Offset offset) { 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); assert(showWhenUnlinked != null);
if (layer == null) { if (layer == null) {
layer = FollowerLayer( layer = FollowerLayer(
link: link, link: link,
showWhenUnlinked: showWhenUnlinked, showWhenUnlinked: showWhenUnlinked,
linkedOffset: this.offset, linkedOffset: effectiveLinkedOffset,
unlinkedOffset: offset, unlinkedOffset: offset,
); );
} else { } else {
layer! layer
..link = link ?..link = link
..showWhenUnlinked = showWhenUnlinked ..showWhenUnlinked = showWhenUnlinked
..linkedOffset = this.offset ..linkedOffset = effectiveLinkedOffset
..unlinkedOffset = offset; ..unlinkedOffset = offset;
} }
context.pushLayer( context.pushLayer(
......
...@@ -1328,9 +1328,11 @@ class CompositedTransformTarget extends SingleChildRenderObjectWidget { ...@@ -1328,9 +1328,11 @@ class CompositedTransformTarget extends SingleChildRenderObjectWidget {
/// ///
/// When this widget is composited during the compositing phase (which comes /// When this widget is composited during the compositing phase (which comes
/// after the paint phase, as described in [WidgetsBinding.drawFrame]), it /// after the paint phase, as described in [WidgetsBinding.drawFrame]), it
/// applies a transformation that causes it to provide its child with a /// applies a transformation that brings [targetAnchor] of the linked
/// coordinate space that matches that of the linked [CompositedTransformTarget] /// [CompositedTransformTarget] and [followerAnchor] of this widget together.
/// widget, offset by [offset]. /// 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.
/// ///
/// The [LayerLink] object used as the [link] must be the same object as that /// The [LayerLink] object used as the [link] must be the same object as that
/// provided to the matching [CompositedTransformTarget]. /// provided to the matching [CompositedTransformTarget].
...@@ -1362,10 +1364,14 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget { ...@@ -1362,10 +1364,14 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget {
required this.link, required this.link,
this.showWhenUnlinked = true, this.showWhenUnlinked = true,
this.offset = Offset.zero, this.offset = Offset.zero,
this.targetAnchor = Alignment.topLeft,
this.followerAnchor = Alignment.topLeft,
Widget? child, Widget? child,
}) : assert(link != null), }) : assert(link != null),
assert(showWhenUnlinked != null), assert(showWhenUnlinked != null),
assert(offset != null), assert(offset != null),
assert(targetAnchor != null),
assert(followerAnchor != null),
super(key: key, child: child); super(key: key, child: child);
/// The link object that connects this [CompositedTransformFollower] with a /// The link object that connects this [CompositedTransformFollower] with a
...@@ -1385,8 +1391,33 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget { ...@@ -1385,8 +1391,33 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget {
/// hidden. /// hidden.
final bool showWhenUnlinked; final bool showWhenUnlinked;
/// The offset to apply to the origin of the linked /// The anchor point on the linked [CompositedTransformTarget] that
/// [CompositedTransformTarget] to obtain this widget's origin. /// [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.
final Offset offset; final Offset offset;
@override @override
...@@ -1395,6 +1426,8 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget { ...@@ -1395,6 +1426,8 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget {
link: link, link: link,
showWhenUnlinked: showWhenUnlinked, showWhenUnlinked: showWhenUnlinked,
offset: offset, offset: offset,
leaderAnchor: targetAnchor,
followerAnchor: followerAnchor,
); );
} }
...@@ -1403,7 +1436,9 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget { ...@@ -1403,7 +1436,9 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget {
renderObject renderObject
..link = link ..link = link
..showWhenUnlinked = showWhenUnlinked ..showWhenUnlinked = showWhenUnlinked
..offset = offset; ..offset = offset
..leaderAnchor = targetAnchor
..followerAnchor = followerAnchor;
} }
} }
......
...@@ -9,11 +9,58 @@ import 'package:flutter/rendering.dart'; ...@@ -9,11 +9,58 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
void main() { void main() {
testWidgets('Composited transforms - only offsets', (WidgetTester tester) async {
final LayerLink link = LayerLink(); final LayerLink link = LayerLink();
testWidgets('Change link during layout', (WidgetTester tester) async {
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
await tester.pumpWidget( Widget build({ LayerLink linkToUse }) {
Directionality( return Directionality(
textDirection: TextDirection.ltr,
// The LayoutBuilder forces the CompositedTransformTarget widget to
// access its own size when [RenderObject.debugActiveLayout] is
// non-null.
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Stack(
children: <Widget>[
Positioned(
left: 123.0,
top: 456.0,
child: CompositedTransformTarget(
link: linkToUse ?? link,
child: const SizedBox(height: 10.0, width: 10.0),
),
),
Positioned(
left: 787.0,
top: 343.0,
child: CompositedTransformFollower(
link: linkToUse ?? link,
targetAnchor: Alignment.center,
followerAnchor: Alignment.center,
child: Container(key: key, height: 20.0, width: 20.0),
),
),
],
);
},
),
);
}
await tester.pumpWidget(build());
final RenderBox box = key.currentContext.findRenderObject() as RenderBox;
expect(box.localToGlobal(Offset.zero), const Offset(118.0, 451.0));
await tester.pumpWidget(build(linkToUse: LayerLink()));
expect(box.localToGlobal(Offset.zero), const Offset(118.0, 451.0));
});
group('Composited transforms - only offsets', () {
final GlobalKey key = GlobalKey();
Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) {
return Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
...@@ -30,23 +77,41 @@ void main() { ...@@ -30,23 +77,41 @@ void main() {
top: 343.0, top: 343.0,
child: CompositedTransformFollower( child: CompositedTransformFollower(
link: link, link: link,
child: Container(key: key, height: 10.0, width: 10.0), targetAnchor: targetAlignment,
followerAnchor: followerAlignment,
child: Container(key: key, height: 20.0, width: 20.0),
), ),
), ),
], ],
), ),
),
); );
}
testWidgets('topLeft', (WidgetTester tester) async {
await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft));
final RenderBox box = key.currentContext.findRenderObject() as RenderBox; final RenderBox box = key.currentContext.findRenderObject() as RenderBox;
expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0)); expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0));
}); });
testWidgets('Composited transforms - with rotations', (WidgetTester tester) async { testWidgets('center', (WidgetTester tester) async {
final LayerLink link = LayerLink(); 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));
});
});
group('Composited transforms - with rotations', () {
final GlobalKey key1 = GlobalKey(); final GlobalKey key1 = GlobalKey();
final GlobalKey key2 = GlobalKey(); final GlobalKey key2 = GlobalKey();
await tester.pumpWidget(
Directionality( Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) {
return Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
...@@ -57,7 +122,7 @@ void main() { ...@@ -57,7 +122,7 @@ void main() {
angle: 1.0, // radians angle: 1.0, // radians
child: CompositedTransformTarget( child: CompositedTransformTarget(
link: link, link: link,
child: Container(key: key1, height: 10.0, width: 10.0), child: Container(key: key1, width: 80.0, height: 10.0),
), ),
), ),
), ),
...@@ -68,28 +133,50 @@ void main() { ...@@ -68,28 +133,50 @@ void main() {
angle: -0.3, // radians angle: -0.3, // radians
child: CompositedTransformFollower( child: CompositedTransformFollower(
link: link, link: link,
child: Container(key: key2, height: 10.0, width: 10.0), targetAnchor: targetAlignment,
followerAnchor: followerAlignment,
child: Container(key: key2, width: 40.0, height: 20.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 box1 = key1.currentContext.findRenderObject() as RenderBox;
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox; final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
final Offset position1 = box1.localToGlobal(Offset.zero); final Offset position1 = box1.localToGlobal(Offset.zero);
final Offset position2 = box2.localToGlobal(Offset.zero); final Offset position2 = box2.localToGlobal(Offset.zero);
expect(position1.dx, moreOrLessEquals(position2.dx)); expect(position1, offsetMoreOrLessEquals(position2));
expect(position1.dy, moreOrLessEquals(position2.dy));
}); });
testWidgets('Composited transforms - nested', (WidgetTester tester) async { testWidgets('center', (WidgetTester tester) async {
final LayerLink link = LayerLink(); 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));
});
});
group('Composited transforms - nested', () {
final GlobalKey key1 = GlobalKey(); final GlobalKey key1 = GlobalKey();
final GlobalKey key2 = GlobalKey(); final GlobalKey key2 = GlobalKey();
await tester.pumpWidget(
Directionality( Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) {
return Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
...@@ -100,7 +187,7 @@ void main() { ...@@ -100,7 +187,7 @@ void main() {
angle: 1.0, // radians angle: 1.0, // radians
child: CompositedTransformTarget( child: CompositedTransformTarget(
link: link, link: link,
child: Container(key: key1, height: 10.0, width: 10.0), child: Container(key: key1, width: 80.0, height: 10.0),
), ),
), ),
), ),
...@@ -119,7 +206,9 @@ void main() { ...@@ -119,7 +206,9 @@ void main() {
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: CompositedTransformFollower( child: CompositedTransformFollower(
link: link, link: link,
child: Container(key: key2, height: 10.0, width: 10.0), targetAnchor: targetAlignment,
followerAnchor: followerAlignment,
child: Container(key: key2, width: 40.0, height: 20.0),
), ),
), ),
), ),
...@@ -129,24 +218,45 @@ void main() { ...@@ -129,24 +218,45 @@ 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 box1 = key1.currentContext.findRenderObject() as RenderBox;
final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox; final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
final Offset position1 = box1.localToGlobal(Offset.zero); final Offset position1 = box1.localToGlobal(Offset.zero);
final Offset position2 = box2.localToGlobal(Offset.zero); final Offset position2 = box2.localToGlobal(Offset.zero);
expect(position1.dx, moreOrLessEquals(position2.dx)); expect(position1, offsetMoreOrLessEquals(position2));
expect(position1.dy, moreOrLessEquals(position2.dy));
}); });
testWidgets('Composited transforms - hit testing', (WidgetTester tester) async { testWidgets('center', (WidgetTester tester) async {
final LayerLink link = LayerLink(); 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));
});
});
group('Composited transforms - hit testing', () {
final GlobalKey key1 = GlobalKey(); final GlobalKey key1 = GlobalKey();
final GlobalKey key2 = GlobalKey(); final GlobalKey key2 = GlobalKey();
final GlobalKey key3 = GlobalKey(); final GlobalKey key3 = GlobalKey();
bool _tapped = false;
await tester.pumpWidget( bool tapped = false;
Directionality(
Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) {
return Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
...@@ -163,18 +273,34 @@ void main() { ...@@ -163,18 +273,34 @@ void main() {
child: GestureDetector( child: GestureDetector(
key: key2, key: key2,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { _tapped = true; }, onTap: () { tapped = true; },
child: Container(key: key3, height: 10.0, width: 10.0), child: Container(key: key3, height: 2.0, width: 2.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; final RenderBox box2 = key2.currentContext.findRenderObject() as RenderBox;
expect(box2.size, const Size(10.0, 10.0)); expect(box2.size, const Size(2.0, 2.0));
expect(_tapped, isFalse); expect(tapped, isFalse);
await tester.tap(find.byKey(key1)); await tester.tap(find.byKey(key3));
expect(_tapped, isTrue); expect(tapped, isTrue);
});
}
}
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment