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

import 'package:flutter/material.dart';
6
import 'package:flutter/services.dart';
7 8 9 10 11
import 'package:flutter_test/flutter_test.dart';

import '../rendering/mock_canvas.dart';

void main() {
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
  testWidgets('The Ink widget renders a Container by default', (WidgetTester tester) async {
    await tester.pumpWidget(
      Material(
        child: Ink(),
      ),
    );
    expect(tester.getSize(find.byType(Container)).height, 600.0);
    expect(tester.getSize(find.byType(Container)).width, 800.0);

    const double height = 150.0;
    const double width = 200.0;
    await tester.pumpWidget(
      Material(
        child: Center( // used to constrain to child's size
          child: Ink(
            height: height,
            width: width,
          ),
        ),
      ),
    );
    await tester.pumpAndSettle();
    expect(tester.getSize(find.byType(Container)).height, height);
    expect(tester.getSize(find.byType(Container)).width, width);
  });

Ian Hickson's avatar
Ian Hickson committed
38
  testWidgets('The InkWell widget renders an ink splash', (WidgetTester tester) async {
39 40
    const Color highlightColor = Color(0xAAFF0000);
    const Color splashColor = Color(0xAA0000FF);
41
    const BorderRadius borderRadius = BorderRadius.all(Radius.circular(6.0));
42 43

    await tester.pumpWidget(
44
      Directionality(
45
        textDirection: TextDirection.ltr,
46 47
        child: Material(
          child: Center(
48
            child: SizedBox(
49 50
              width: 200.0,
              height: 60.0,
51
              child: InkWell(
52 53 54 55 56
                borderRadius: borderRadius,
                highlightColor: highlightColor,
                splashColor: splashColor,
                onTap: () { },
              ),
57 58 59 60 61 62
            ),
          ),
        ),
      ),
    );

63
    final Offset center = tester.getCenter(find.byType(InkWell));
64 65
    final TestGesture gesture = await tester.startGesture(center);
    await tester.pump(); // start gesture
66
    await tester.pump(const Duration(milliseconds: 200)); // wait for splash to be well under way
67

68
    final RenderBox box = Material.of(tester.element(find.byType(InkWell)))! as RenderBox;
69 70 71
    expect(
      box,
      paints
72 73 74
        ..translate(x: 0.0, y: 0.0)
        ..save()
        ..translate(x: 300.0, y: 270.0)
75
        ..clipRRect(rrect: RRect.fromLTRBR(0.0, 0.0, 200.0, 60.0, const Radius.circular(6.0)))
76 77
        ..circle(x: 100.0, y: 30.0, radius: 21.0, color: splashColor)
        ..restore()
78
        ..rrect(
79
          rrect: RRect.fromLTRBR(300.0, 270.0, 500.0, 330.0, const Radius.circular(6.0)),
80
          color: highlightColor,
81
        ),
82 83 84 85
    );

    await gesture.up();
  });
86

Ian Hickson's avatar
Ian Hickson committed
87
  testWidgets('The InkWell widget renders an ink ripple', (WidgetTester tester) async {
88 89
    const Color highlightColor = Color(0xAAFF0000);
    const Color splashColor = Color(0xB40000FF);
90
    const BorderRadius borderRadius = BorderRadius.all(Radius.circular(6.0));
91 92

    await tester.pumpWidget(
93
      Directionality(
94
        textDirection: TextDirection.ltr,
95 96
        child: Material(
          child: Center(
97
            child: SizedBox(
98 99
              width: 100.0,
              height: 100.0,
100
              child: InkWell(
101 102 103 104 105 106 107
                borderRadius: borderRadius,
                highlightColor: highlightColor,
                splashColor: splashColor,
                onTap: () { },
                radius: 100.0,
                splashFactory: InkRipple.splashFactory,
              ),
108 109 110 111 112 113 114 115 116 117 118 119
            ),
          ),
        ),
      ),
    );

    final Offset tapDownOffset = tester.getTopLeft(find.byType(InkWell));
    final Offset inkWellCenter = tester.getCenter(find.byType(InkWell));
    //final TestGesture gesture = await tester.startGesture(tapDownOffset);
    await tester.tapAt(tapDownOffset);
    await tester.pump(); // start gesture

120
    final RenderBox box = Material.of(tester.element(find.byType(InkWell)))! as RenderBox;
121 122 123 124

    bool offsetsAreClose(Offset a, Offset b) => (a - b).distance < 1.0;
    bool radiiAreClose(double a, double b) => (a - b).abs() < 1.0;

125 126 127 128 129
    PaintPattern ripplePattern(Offset expectedCenter, double expectedRadius, int expectedAlpha) {
      return paints
        ..translate(x: 0.0, y: 0.0)
        ..translate(x: tapDownOffset.dx, y: tapDownOffset.dy)
        ..something((Symbol method, List<dynamic> arguments) {
130
          if (method != #drawCircle) {
131
            return false;
132
          }
133 134 135
          final Offset center = arguments[0] as Offset;
          final double radius = arguments[1] as double;
          final Paint paint = arguments[2] as Paint;
136
          if (offsetsAreClose(center, expectedCenter) && radiiAreClose(radius, expectedRadius) && paint.color.alpha == expectedAlpha) {
137
            return true;
138
          }
139 140 141
          throw '''
            Expected: center == $expectedCenter, radius == $expectedRadius, alpha == $expectedAlpha
            Found: center == $center radius == $radius alpha == ${paint.color.alpha}''';
142
        }
143 144 145
      );
    }

Ian Hickson's avatar
Ian Hickson committed
146
    // Initially the ripple's center is where the tap occurred;
147 148
    // ripplePattern always add a translation of tapDownOffset.
    expect(box, ripplePattern(Offset.zero, 30.0, 0));
149 150

    // The ripple fades in for 75ms. During that time its alpha is eased from
151
    // 0 to the splashColor's alpha value and its center moves towards the
152 153
    // center of the ink well.
    await tester.pump(const Duration(milliseconds: 50));
154
    expect(box, ripplePattern(const Offset(17.0, 17.0), 56.0, 120));
155 156 157 158

    // At 75ms the ripple has fade in: it's alpha matches the splashColor's
    // alpha and its center has moved closer to the ink well's center.
    await tester.pump(const Duration(milliseconds: 25));
159
    expect(box, ripplePattern(const Offset(29.0, 29.0), 73.0, 180));
160 161 162 163

    // At this point the splash radius has expanded to its limit: 5 past the
    // ink well's radius parameter. The splash center has moved to its final
    // location at the inkwell's center and the fade-out is about to start.
164 165
    // The fade-out begins at 225ms = 50ms + 25ms + 150ms.
    await tester.pump(const Duration(milliseconds: 150));
166
    expect(box, ripplePattern(inkWellCenter - tapDownOffset, 105.0, 180));
167 168 169

    // After another 150ms the fade-out is complete.
    await tester.pump(const Duration(milliseconds: 150));
170
    expect(box, ripplePattern(inkWellCenter - tapDownOffset, 105.0, 0));
Ian Hickson's avatar
Ian Hickson committed
171 172 173 174
  });

  testWidgets('Does the Ink widget render anything', (WidgetTester tester) async {
    await tester.pumpWidget(
175
      Directionality(
176
        textDirection: TextDirection.ltr,
177 178 179
        child: Material(
          child: Center(
            child: Ink(
180 181 182
              color: Colors.blue,
              width: 200.0,
              height: 200.0,
183
              child: InkWell(
184 185 186
                splashColor: Colors.green,
                onTap: () { },
              ),
Ian Hickson's avatar
Ian Hickson committed
187 188 189 190 191
            ),
          ),
        ),
      ),
    );
192

Ian Hickson's avatar
Ian Hickson committed
193 194 195 196 197
    final Offset center = tester.getCenter(find.byType(InkWell));
    final TestGesture gesture = await tester.startGesture(center);
    await tester.pump(); // start gesture
    await tester.pump(const Duration(milliseconds: 200)); // wait for splash to be well under way

198
    final RenderBox box = Material.of(tester.element(find.byType(InkWell)))! as RenderBox;
Ian Hickson's avatar
Ian Hickson committed
199 200 201
    expect(
      box,
      paints
Dan Field's avatar
Dan Field committed
202
        ..rect(rect: const Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), color: Color(Colors.blue.value))
203
        ..circle(color: Color(Colors.green.value)),
Ian Hickson's avatar
Ian Hickson committed
204 205 206
    );

    await tester.pumpWidget(
207
      Directionality(
208
        textDirection: TextDirection.ltr,
209 210 211
        child: Material(
          child: Center(
            child: Ink(
212 213 214
              color: Colors.red,
              width: 200.0,
              height: 200.0,
215
              child: InkWell(
216 217 218
                splashColor: Colors.green,
                onTap: () { },
              ),
Ian Hickson's avatar
Ian Hickson committed
219 220 221 222 223 224 225 226 227 228 229
            ),
          ),
        ),
      ),
    );

    expect(Material.of(tester.element(find.byType(InkWell))), same(box));

    expect(
      box,
      paints
Dan Field's avatar
Dan Field committed
230
        ..rect(rect: const Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), color: Color(Colors.red.value))
231
        ..circle(color: Color(Colors.green.value)),
Ian Hickson's avatar
Ian Hickson committed
232 233 234
    );

    await tester.pumpWidget(
235
      Directionality(
236
        textDirection: TextDirection.ltr,
237 238 239
        child: Material(
          child: Center(
            child: InkWell( // this is at a different depth in the tree so it's now a new InkWell
240 241 242
              splashColor: Colors.green,
              onTap: () { },
            ),
Ian Hickson's avatar
Ian Hickson committed
243 244 245 246 247 248 249 250 251 252 253
          ),
        ),
      ),
    );

    expect(Material.of(tester.element(find.byType(InkWell))), same(box));

    expect(box, isNot(paints..rect()));
    expect(box, isNot(paints..circle()));

    await gesture.up();
254
  });
255

256
  testWidgets('The InkWell widget renders an SelectAction or ActivateAction-induced ink ripple', (WidgetTester tester) async {
257 258
    const Color highlightColor = Color(0xAAFF0000);
    const Color splashColor = Color(0xB40000FF);
259
    const BorderRadius borderRadius = BorderRadius.all(Radius.circular(6.0));
260 261

    final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
262
    Future<void> buildTest(Intent intent) async {
263
      return tester.pumpWidget(
264
        Shortcuts(
265 266
          shortcuts: <ShortcutActivator, Intent>{
            const SingleActivator(LogicalKeyboardKey.space): intent,
267 268 269 270 271
          },
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Material(
              child: Center(
272
                child: SizedBox(
273 274 275 276 277 278 279 280 281 282 283
                  width: 100.0,
                  height: 100.0,
                  child: InkWell(
                    borderRadius: borderRadius,
                    highlightColor: highlightColor,
                    splashColor: splashColor,
                    focusNode: focusNode,
                    onTap: () { },
                    radius: 100.0,
                    splashFactory: InkRipple.splashFactory,
                  ),
284 285 286 287 288
                ),
              ),
            ),
          ),
        ),
289 290
      );
    }
291

292
    await buildTest(const ActivateIntent());
293 294 295
    focusNode.requestFocus();
    await tester.pumpAndSettle();

296 297
    final Offset topLeft = tester.getTopLeft(find.byType(InkWell));
    final Offset inkWellCenter = tester.getCenter(find.byType(InkWell)) - topLeft;
298 299 300 301 302 303 304 305 306 307 308 309

    bool offsetsAreClose(Offset a, Offset b) => (a - b).distance < 1.0;
    bool radiiAreClose(double a, double b) => (a - b).abs() < 1.0;

    PaintPattern ripplePattern(double expectedRadius, int expectedAlpha) {
      return paints
        ..translate(x: 0.0, y: 0.0)
        ..translate(x: topLeft.dx, y: topLeft.dy)
        ..something((Symbol method, List<dynamic> arguments) {
          if (method != #drawCircle) {
            return false;
          }
310 311 312
          final Offset center = arguments[0] as Offset;
          final double radius = arguments[1] as double;
          final Paint paint = arguments[2] as Paint;
313 314 315 316 317 318 319 320 321 322 323 324
          if (offsetsAreClose(center, inkWellCenter) &&
              radiiAreClose(radius, expectedRadius) &&
              paint.color.alpha == expectedAlpha) {
            return true;
          }
          throw '''
            Expected: center == $inkWellCenter, radius == $expectedRadius, alpha == $expectedAlpha
            Found: center == $center radius == $radius alpha == ${paint.color.alpha}''';
        },
        );
    }

325
    await buildTest(const ActivateIntent());
326 327 328 329
    await tester.pumpAndSettle();
    await tester.sendKeyEvent(LogicalKeyboardKey.space);
    await tester.pump();

330
    final RenderBox box = Material.of(tester.element(find.byType(InkWell)))! as RenderBox;
331

332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
    // ripplePattern always add a translation of topLeft.
    expect(box, ripplePattern(30.0, 0));

    // The ripple fades in for 75ms. During that time its alpha is eased from
    // 0 to the splashColor's alpha value.
    await tester.pump(const Duration(milliseconds: 50));
    expect(box, ripplePattern(56.0, 120));

    // At 75ms the ripple has faded in: it's alpha matches the splashColor's
    // alpha.
    await tester.pump(const Duration(milliseconds: 25));
    expect(box, ripplePattern(73.0, 180));

    // At this point the splash radius has expanded to its limit: 5 past the
    // ink well's radius parameter. The fade-out is about to start.
    // The fade-out begins at 225ms = 50ms + 25ms + 150ms.
    await tester.pump(const Duration(milliseconds: 150));
    expect(box, ripplePattern(105.0, 180));

    // After another 150ms the fade-out is complete.
    await tester.pump(const Duration(milliseconds: 150));
    expect(box, ripplePattern(105.0, 0));
  });

356 357 358
  testWidgets('Cancel an InkRipple that was disposed when its animation ended', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/14391
    await tester.pumpWidget(
359
      Directionality(
360
        textDirection: TextDirection.ltr,
361 362
        child: Material(
          child: Center(
363
            child: SizedBox(
364 365
              width: 100.0,
              height: 100.0,
366
              child: InkWell(
367 368 369 370
                onTap: () { },
                radius: 100.0,
                splashFactory: InkRipple.splashFactory,
              ),
371 372 373 374 375 376 377 378 379 380 381 382 383
            ),
          ),
        ),
      ),
    );

    final Offset tapDownOffset = tester.getTopLeft(find.byType(InkWell));
    await tester.tapAt(tapDownOffset);
    await tester.pump(); // start splash
    await tester.pump(const Duration(milliseconds: 375)); // _kFadeOutDuration, in_ripple.dart

    final TestGesture gesture = await tester.startGesture(tapDownOffset);
    await tester.pump(); // start gesture
384
    await gesture.moveTo(Offset.zero);
385 386 387
    await gesture.up(); // generates a tap cancel
    await tester.pumpAndSettle();
  });
388 389

  testWidgets('Cancel an InkRipple that was disposed when its animation ended', (WidgetTester tester) async {
390 391
    const Color highlightColor = Color(0xAAFF0000);
    const Color splashColor = Color(0xB40000FF);
392 393 394

    // Regression test for https://github.com/flutter/flutter/issues/14391
    await tester.pumpWidget(
395
      Directionality(
396
        textDirection: TextDirection.ltr,
397 398
        child: Material(
          child: Center(
399
            child: SizedBox(
400 401
              width: 100.0,
              height: 100.0,
402
              child: InkWell(
403 404 405 406 407 408
                splashColor: splashColor,
                highlightColor: highlightColor,
                onTap: () { },
                radius: 100.0,
                splashFactory: InkRipple.splashFactory,
              ),
409 410 411 412 413 414 415 416 417 418 419 420 421 422
            ),
          ),
        ),
      ),
    );

    final Offset tapDownOffset = tester.getTopLeft(find.byType(InkWell));
    await tester.tapAt(tapDownOffset);
    await tester.pump(); // start splash
    // No delay here so _fadeInController.value=1.0 (InkRipple.dart)

    // Generate a tap cancel; Will cancel the ink splash before it started
    final TestGesture gesture = await tester.startGesture(tapDownOffset);
    await tester.pump(); // start gesture
423
    await gesture.moveTo(Offset.zero);
424 425
    await gesture.up(); // generates a tap cancel

426
    final RenderBox box = Material.of(tester.element(find.byType(InkWell)))! as RenderBox;
427
    expect(box, paints..everything((Symbol method, List<dynamic> arguments) {
428
      if (method != #drawCircle) {
429
        return true;
430
      }
431
      final Paint paint = arguments[2] as Paint;
432
      if (paint.color.alpha == 0) {
433
        return true;
434
      }
435 436 437
      throw 'Expected: paint.color.alpha == 0, found: ${paint.color.alpha}';
    }));
  });
438
}