composited_transform_test.dart 15.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7
import 'dart:ui' as ui;

import 'package:flutter/rendering.dart';
8
import 'package:flutter/widgets.dart';
9
import 'package:flutter_test/flutter_test.dart';
10
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
11 12

void main() {
13 14
  final LayerLink link = LayerLink();

15
  testWidgetsWithLeakTracking('Change link during layout', (WidgetTester tester) async {
16
    final GlobalKey key = GlobalKey();
17
    Widget build({ LayerLink? linkToUse }) {
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
      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,
42
                  child: SizedBox(key: key, height: 20.0, width: 20.0),
43 44 45 46 47 48 49 50 51 52
                ),
              ),
            ],
          );
          },
        ),
      );
    }

    await tester.pumpWidget(build());
53
    final RenderBox box = key.currentContext!.findRenderObject()! as RenderBox;
54 55 56 57 58 59
    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));
  });

60
  testWidgetsWithLeakTracking('LeaderLayer should not cause error', (WidgetTester tester) async {
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
    final LayerLink link = LayerLink();

    Widget buildWidget({
      required double paddingLeft,
      Color siblingColor = const Color(0xff000000),
    }) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Stack(
          children: <Widget>[
            Padding(
              padding: EdgeInsets.only(left: paddingLeft),
              child: CompositedTransformTarget(
                link: link,
                child: RepaintBoundary(child: ClipRect(child: Container(color: const Color(0x00ff0000)))),
              ),
            ),
            Positioned.fill(child: RepaintBoundary(child: ColoredBox(color: siblingColor))),
          ],
        ),
      );
    }

    await tester.pumpWidget(buildWidget(paddingLeft: 10));
    await tester.pumpWidget(buildWidget(paddingLeft: 0));
    await tester.pumpWidget(buildWidget(paddingLeft: 0, siblingColor: const Color(0x0000ff00)));
  });

89
  group('Composited transforms - only offsets', () {
90
    final GlobalKey key = GlobalKey();
91

92
    Widget build({ required Alignment targetAlignment, required Alignment followerAlignment }) {
93
      return Directionality(
94
        textDirection: TextDirection.ltr,
95
        child: Stack(
96
          children: <Widget>[
97
            Positioned(
98 99
              left: 123.0,
              top: 456.0,
100
              child: CompositedTransformTarget(
101
                link: link,
102
                child: const SizedBox(height: 10.0, width: 10.0),
103
              ),
104
            ),
105
            Positioned(
106 107
              left: 787.0,
              top: 343.0,
108
              child: CompositedTransformFollower(
109
                link: link,
110 111
                targetAnchor: targetAlignment,
                followerAnchor: followerAlignment,
112
                child: SizedBox(key: key, height: 20.0, width: 20.0),
113
              ),
114
            ),
115 116
          ],
        ),
117 118 119
      );
    }

120
    testWidgetsWithLeakTracking('topLeft', (WidgetTester tester) async {
121
      await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft));
122
      final RenderBox box = key.currentContext!.findRenderObject()! as RenderBox;
123 124 125
      expect(box.localToGlobal(Offset.zero), const Offset(123.0, 456.0));
    });

126
    testWidgetsWithLeakTracking('center', (WidgetTester tester) async {
127
      await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center));
128
      final RenderBox box = key.currentContext!.findRenderObject()! as RenderBox;
129 130 131
      expect(box.localToGlobal(Offset.zero), const Offset(118.0, 451.0));
    });

132
    testWidgetsWithLeakTracking('bottomRight - topRight', (WidgetTester tester) async {
133
      await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight));
134
      final RenderBox box = key.currentContext!.findRenderObject()! as RenderBox;
135 136
      expect(box.localToGlobal(Offset.zero), const Offset(113.0, 466.0));
    });
137 138
  });

139
  group('Composited transforms - with rotations', () {
140 141
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();
142

143
    Widget build({ required Alignment targetAlignment, required Alignment followerAlignment }) {
144
      return Directionality(
145
        textDirection: TextDirection.ltr,
146
        child: Stack(
147
          children: <Widget>[
148
            Positioned(
149 150
              top: 123.0,
              left: 456.0,
151
              child: Transform.rotate(
152
                angle: 1.0, // radians
153
                child: CompositedTransformTarget(
154
                  link: link,
155
                  child: SizedBox(key: key1, width: 80.0, height: 10.0),
156
                ),
157 158
              ),
            ),
159
            Positioned(
160 161
              top: 787.0,
              left: 343.0,
162
              child: Transform.rotate(
163
                angle: -0.3, // radians
164
                child: CompositedTransformFollower(
165
                  link: link,
166 167
                  targetAnchor: targetAlignment,
                  followerAnchor: followerAlignment,
168
                  child: SizedBox(key: key2, width: 40.0, height: 20.0),
169
                ),
170 171
              ),
            ),
172 173
          ],
        ),
174 175
      );
    }
176
    testWidgetsWithLeakTracking('topLeft', (WidgetTester tester) async {
177
      await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft));
178 179
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
180 181 182 183 184
      final Offset position1 = box1.localToGlobal(Offset.zero);
      final Offset position2 = box2.localToGlobal(Offset.zero);
      expect(position1, offsetMoreOrLessEquals(position2));
    });

185
    testWidgetsWithLeakTracking('center', (WidgetTester tester) async {
186
      await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center));
187 188
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
189 190 191 192 193
      final Offset position1 = box1.localToGlobal(const Offset(40, 5));
      final Offset position2 = box2.localToGlobal(const Offset(20, 10));
      expect(position1, offsetMoreOrLessEquals(position2));
    });

194
    testWidgetsWithLeakTracking('bottomRight - topRight', (WidgetTester tester) async {
195
      await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight));
196 197
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
198 199 200 201
      final Offset position1 = box1.localToGlobal(const Offset(80, 10));
      final Offset position2 = box2.localToGlobal(const Offset(40, 0));
      expect(position1, offsetMoreOrLessEquals(position2));
    });
202 203
  });

204
  group('Composited transforms - nested', () {
205 206
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();
207

208
    Widget build({ required Alignment targetAlignment, required Alignment followerAlignment }) {
209
      return Directionality(
210
        textDirection: TextDirection.ltr,
211
        child: Stack(
212
          children: <Widget>[
213
            Positioned(
214 215
              top: 123.0,
              left: 456.0,
216
              child: Transform.rotate(
217
                angle: 1.0, // radians
218
                child: CompositedTransformTarget(
219
                  link: link,
220
                  child: SizedBox(key: key1, width: 80.0, height: 10.0),
221
                ),
222 223
              ),
            ),
224
            Positioned(
225 226
              top: 787.0,
              left: 343.0,
227
              child: Transform.rotate(
228
                angle: -0.3, // radians
229
                child: Padding(
230
                  padding: const EdgeInsets.all(20.0),
231 232 233 234 235
                  child: CompositedTransformFollower(
                    link: LayerLink(),
                    child: Transform(
                      transform: Matrix4.skew(0.9, 1.1),
                      child: Padding(
236
                        padding: const EdgeInsets.all(20.0),
237
                        child: CompositedTransformFollower(
238
                          link: link,
239 240
                          targetAnchor: targetAlignment,
                          followerAnchor: followerAlignment,
241
                          child: SizedBox(key: key2, width: 40.0, height: 20.0),
242
                        ),
243 244 245 246 247 248
                      ),
                    ),
                  ),
                ),
              ),
            ),
249 250
          ],
        ),
251 252
      );
    }
253
    testWidgetsWithLeakTracking('topLeft', (WidgetTester tester) async {
254
      await tester.pumpWidget(build(targetAlignment: Alignment.topLeft, followerAlignment: Alignment.topLeft));
255 256
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
257 258 259 260 261
      final Offset position1 = box1.localToGlobal(Offset.zero);
      final Offset position2 = box2.localToGlobal(Offset.zero);
      expect(position1, offsetMoreOrLessEquals(position2));
    });

262
    testWidgetsWithLeakTracking('center', (WidgetTester tester) async {
263
      await tester.pumpWidget(build(targetAlignment: Alignment.center, followerAlignment: Alignment.center));
264 265
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
266 267 268 269 270
      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));
    });

271
    testWidgetsWithLeakTracking('bottomRight - topRight', (WidgetTester tester) async {
272
      await tester.pumpWidget(build(targetAlignment: Alignment.bottomRight, followerAlignment: Alignment.topRight));
273 274
      final RenderBox box1 = key1.currentContext!.findRenderObject()! as RenderBox;
      final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
275 276 277 278
      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));
    });
279 280
  });

281
  group('Composited transforms - hit testing', () {
282 283 284
    final GlobalKey key1 = GlobalKey();
    final GlobalKey key2 = GlobalKey();
    final GlobalKey key3 = GlobalKey();
285 286 287

    bool tapped = false;

288
    Widget build({ required Alignment targetAlignment, required Alignment followerAlignment }) {
289
      return Directionality(
290
        textDirection: TextDirection.ltr,
291
        child: Stack(
292
          children: <Widget>[
293
            Positioned(
294 295
              left: 123.0,
              top: 456.0,
296
              child: CompositedTransformTarget(
297
                link: link,
298
                child: SizedBox(key: key1, height: 10.0, width: 10.0),
299
              ),
300
            ),
301
            CompositedTransformFollower(
302
              link: link,
303
              child: GestureDetector(
304 305
                key: key2,
                behavior: HitTestBehavior.opaque,
306
                onTap: () { tapped = true; },
307
                child: SizedBox(key: key3, height: 2.0, width: 2.0),
308
              ),
309
            ),
310 311
          ],
        ),
312 313 314 315 316 317 318 319 320 321 322 323 324
      );
    }

    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) {
325
        testWidgetsWithLeakTracking('$targetAlignment - $followerAlignment', (WidgetTester tester) async{
326
          await tester.pumpWidget(build(targetAlignment: targetAlignment, followerAlignment: followerAlignment));
327
          final RenderBox box2 = key2.currentContext!.findRenderObject()! as RenderBox;
328 329
          expect(box2.size, const Size(2.0, 2.0));
          expect(tapped, isFalse);
330
          await tester.tap(find.byKey(key3), warnIfMissed: false); // the container itself is transparent to hits
331 332 333 334
          expect(tapped, isTrue);
        });
      }
    }
335
  });
336

337
  testWidgetsWithLeakTracking('Leader after Follower asserts', (WidgetTester tester) async {
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
    final LayerLink link = LayerLink();
    await tester.pumpWidget(
      CompositedTransformFollower(
        link: link,
        child: CompositedTransformTarget(
          link: link,
          child: const SizedBox(height: 20, width: 20),
        ),
      ),
    );

    expect(
      (tester.takeException() as AssertionError).message,
      contains('LeaderLayer anchor must come before FollowerLayer in paint order'),
    );
  });
354

355
  testWidgetsWithLeakTracking(
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
      '`FollowerLayer` (`CompositedTransformFollower`) has null pointer error when using with some kinds of `Layer`s',
      (WidgetTester tester) async {
    final LayerLink link = LayerLink();
    await tester.pumpWidget(
      CompositedTransformTarget(
        link: link,
        child: CompositedTransformFollower(
          link: link,
          child: const _CustomWidget(),
        ),
      ),
    );
  });
}

class _CustomWidget extends SingleChildRenderObjectWidget {
372
  const _CustomWidget();
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412

  @override
  _CustomRenderObject createRenderObject(BuildContext context) => _CustomRenderObject();

  @override
  void updateRenderObject(BuildContext context, _CustomRenderObject renderObject) {}
}

class _CustomRenderObject extends RenderProxyBox {
  _CustomRenderObject({RenderBox? child}) : super(child);

  @override
  void paint(PaintingContext context, Offset offset) {
    if (layer == null) {
      layer = _CustomLayer(
        computeSomething: _computeSomething,
      );
    } else {
      (layer as _CustomLayer?)?.computeSomething = _computeSomething;
    }

    context.pushLayer(layer!, super.paint, Offset.zero);
  }

  void _computeSomething() {
    // indeed, use `globalToLocal` to compute some useful data
    globalToLocal(Offset.zero);
  }
}

class _CustomLayer extends ContainerLayer {
  _CustomLayer({required this.computeSomething});

  VoidCallback computeSomething;

  @override
  void addToScene(ui.SceneBuilder builder) {
    computeSomething(); // indeed, need to use result of this function
    super.addToScene(builder);
  }
413
}