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 6
// 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';
import 'package:flutter/rendering.dart';
7
import 'package:flutter/services.dart';
8 9 10 11 12
import 'package:flutter_test/flutter_test.dart';

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

void main() {
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 38
  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
39
  testWidgets('The InkWell widget renders an ink splash', (WidgetTester tester) async {
40 41
    const Color highlightColor = Color(0xAAFF0000);
    const Color splashColor = Color(0xAA0000FF);
42
    final BorderRadius borderRadius = BorderRadius.circular(6.0);
43 44

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

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

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

    await gesture.up();
  });
87

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

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

    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

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

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

126 127 128 129 130
    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) {
131
          if (method != #drawCircle)
132
            return false;
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 138 139 140
            return true;
          throw '''
            Expected: center == $expectedCenter, radius == $expectedRadius, alpha == $expectedAlpha
            Found: center == $center radius == $radius alpha == ${paint.color.alpha}''';
141
        }
142 143 144
      );
    }

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

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

    // 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));
158
    expect(box, ripplePattern(const Offset(29.0, 29.0), 73.0, 180));
159 160 161 162

    // 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.
163 164
    // The fade-out begins at 225ms = 50ms + 25ms + 150ms.
    await tester.pump(const Duration(milliseconds: 150));
165
    expect(box, ripplePattern(inkWellCenter - tapDownOffset, 105.0, 180));
166 167 168

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

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

Ian Hickson's avatar
Ian Hickson committed
192 193 194 195 196
    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

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

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

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

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

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

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

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

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

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

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

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

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

    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;
          }
309 310 311
          final Offset center = arguments[0] as Offset;
          final double radius = arguments[1] as double;
          final Paint paint = arguments[2] as Paint;
312 313 314 315 316 317 318 319 320 321 322 323
          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}''';
        },
        );
    }

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

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

331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
    // 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));
  });

355 356 357
  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(
358
      Directionality(
359
        textDirection: TextDirection.ltr,
360 361 362
        child: Material(
          child: Center(
            child: Container(
363 364
              width: 100.0,
              height: 100.0,
365
              child: InkWell(
366 367 368 369
                onTap: () { },
                radius: 100.0,
                splashFactory: InkRipple.splashFactory,
              ),
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
            ),
          ),
        ),
      ),
    );

    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
    await gesture.moveTo(const Offset(0.0, 0.0));
    await gesture.up(); // generates a tap cancel
    await tester.pumpAndSettle();
  });
387 388

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

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

    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
    await gesture.moveTo(const Offset(0.0, 0.0));
    await gesture.up(); // generates a tap cancel

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