From 2fbb529d2b04ab8a3472e14c4efdee9a068ea0f4 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Mon, 14 Sep 2020 17:52:06 -0700 Subject: [PATCH] Add CompositedTransformFollower.{followerAnchor, leaderAnchor} for custom anchoring (#64930) --- packages/flutter/lib/src/rendering/layer.dart | 101 +++++++--- .../flutter/lib/src/rendering/proxy_box.dart | 81 +++++++- packages/flutter/lib/src/widgets/basic.dart | 47 ++++- .../widgets/composited_transform_test.dart | 182 +++++++++++++----- 4 files changed, 314 insertions(+), 97 deletions(-) diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index 83d789bc55c..507198a509c 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:collection'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; @@ -2028,6 +2027,14 @@ 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 ? "" : "" })'; } @@ -2266,59 +2273,91 @@ 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. - Matrix4 _collectTransformForLayerChain(List layers) { + static Matrix4 _collectTransformForLayerChain(List 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 ancestorsA, List 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 (link.leader == null) + if (leader == null) return; // 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(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 ancestors = HashSet(); - 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 forwardLayers = [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 inverseLayers = [layer]; - do { - layer = layer!.parent; - inverseLayers.add(layer!); - } while (layer != ancestor); - // Establish the forward and backward matrices given these lists of layers. + 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 forwardLayers = [leader]; + // Stores [this (follower), ..., commonAncestor] after calling + // _pathsToCommonAncestor. + final List inverseLayers = [this]; + + final Layer? ancestor = _pathsToCommonAncestor( + leader, this, + forwardLayers, inverseLayers, + ); + assert(ancestor != null); + 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; } diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 950c656f9b8..63211d4015f 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -4826,28 +4826,37 @@ class RenderLeaderLayer extends RenderProxyBox { required LayerLink link, RenderBox? child, }) : assert(link != null), - super(child) { - this.link = link; - } + _link = link, + super(child); /// 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) { @@ -4890,6 +4899,8 @@ 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), @@ -4897,6 +4908,8 @@ 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 @@ -4942,6 +4955,46 @@ 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; @@ -4990,19 +5043,29 @@ 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: this.offset, + linkedOffset: effectiveLinkedOffset, unlinkedOffset: offset, ); } else { - layer! - ..link = link + layer + ?..link = link ..showWhenUnlinked = showWhenUnlinked - ..linkedOffset = this.offset + ..linkedOffset = effectiveLinkedOffset ..unlinkedOffset = offset; } context.pushLayer( diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 1cd427a11b8..9e12304c0a1 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -1324,9 +1324,11 @@ 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 causes it to provide its child with a -/// coordinate space that matches that of the linked [CompositedTransformTarget] -/// widget, offset by [offset]. +/// 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. /// /// The [LayerLink] object used as the [link] must be the same object as that /// provided to the matching [CompositedTransformTarget]. @@ -1358,10 +1360,14 @@ 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 @@ -1381,8 +1387,33 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget { /// hidden. final bool showWhenUnlinked; - /// The offset to apply to the origin of the linked - /// [CompositedTransformTarget] to obtain this widget's origin. + /// 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. final Offset offset; @override @@ -1391,6 +1422,8 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget { link: link, showWhenUnlinked: showWhenUnlinked, offset: offset, + leaderAnchor: targetAnchor, + followerAnchor: followerAnchor, ); } @@ -1399,7 +1432,9 @@ class CompositedTransformFollower extends SingleChildRenderObjectWidget { renderObject ..link = link ..showWhenUnlinked = showWhenUnlinked - ..offset = offset; + ..offset = offset + ..leaderAnchor = targetAnchor + ..followerAnchor = followerAnchor; } } diff --git a/packages/flutter/test/widgets/composited_transform_test.dart b/packages/flutter/test/widgets/composited_transform_test.dart index 5e40512f53c..fb9e388552b 100644 --- a/packages/flutter/test/widgets/composited_transform_test.dart +++ b/packages/flutter/test/widgets/composited_transform_test.dart @@ -9,11 +9,12 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; void main() { - testWidgets('Composited transforms - only offsets', (WidgetTester tester) async { - final LayerLink link = LayerLink(); + final LayerLink link = LayerLink(); + group('Composited transforms - only offsets', () { final GlobalKey key = GlobalKey(); - await tester.pumpWidget( - Directionality( + + Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) { + return Directionality( textDirection: TextDirection.ltr, child: Stack( children: [ @@ -30,23 +31,41 @@ void main() { top: 343.0, child: CompositedTransformFollower( 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), ), ), ], ), - ), - ); - final RenderBox box = key.currentContext.findRenderObject() as RenderBox; - expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.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)); + }); }); - testWidgets('Composited transforms - with rotations', (WidgetTester tester) async { - final LayerLink link = LayerLink(); + group('Composited transforms - with rotations', () { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); - await tester.pumpWidget( - Directionality( + + Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) { + return Directionality( textDirection: TextDirection.ltr, child: Stack( children: [ @@ -57,7 +76,7 @@ void main() { angle: 1.0, // radians child: CompositedTransformTarget( link: link, - child: Container(key: key1, height: 10.0, width: 10.0), + child: Container(key: key1, width: 80.0, height: 10.0), ), ), ), @@ -68,28 +87,50 @@ void main() { angle: -0.3, // radians child: CompositedTransformFollower( 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), ), ), ), ], ), - ), - ); - 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)); + ); + } + 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)); + }); }); - testWidgets('Composited transforms - nested', (WidgetTester tester) async { - final LayerLink link = LayerLink(); + group('Composited transforms - nested', () { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); - await tester.pumpWidget( - Directionality( + + Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) { + return Directionality( textDirection: TextDirection.ltr, child: Stack( children: [ @@ -100,7 +141,7 @@ void main() { angle: 1.0, // radians child: CompositedTransformTarget( link: link, - child: Container(key: key1, height: 10.0, width: 10.0), + child: Container(key: key1, width: 80.0, height: 10.0), ), ), ), @@ -119,7 +160,9 @@ void main() { padding: const EdgeInsets.all(20.0), child: CompositedTransformFollower( 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 +172,45 @@ void main() { ), ], ), - ), - ); - 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)); + ); + } + 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)); + }); }); - testWidgets('Composited transforms - hit testing', (WidgetTester tester) async { - final LayerLink link = LayerLink(); + group('Composited transforms - hit testing', () { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final GlobalKey key3 = GlobalKey(); - bool _tapped = false; - await tester.pumpWidget( - Directionality( + + bool tapped = false; + + Widget build({ @required Alignment targetAlignment, @required Alignment followerAlignment }) { + return Directionality( textDirection: TextDirection.ltr, child: Stack( children: [ @@ -163,18 +227,34 @@ void main() { child: GestureDetector( key: key2, behavior: HitTestBehavior.opaque, - onTap: () { _tapped = true; }, - child: Container(key: key3, height: 10.0, width: 10.0), + onTap: () { tapped = true; }, + child: Container(key: key3, height: 2.0, width: 2.0), ), ), ], ), - ), - ); - 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); + ); + } + + const List alignments = [ + 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); + }); + } + } }); }