scroll_physics_test.dart 13.1 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
import 'package:flutter/foundation.dart';
6
import 'package:flutter/material.dart';
7
import 'package:flutter_test/flutter_test.dart';
8
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
9 10

class TestScrollPhysics extends ScrollPhysics {
11
  const TestScrollPhysics({
12
    required this.name,
13 14
    super.parent,
  });
15 16 17
  final String name;

  @override
18
  TestScrollPhysics applyTo(ScrollPhysics? ancestor) {
19 20
    return TestScrollPhysics(
      name: name,
21
      parent: parent?.applyTo(ancestor) ?? ancestor!,
22
    );
23 24
  }

25
  TestScrollPhysics get namedParent => parent! as TestScrollPhysics;
26 27 28 29
  String get names => parent == null ? name : '$name ${namedParent.names}';

  @override
  String toString() {
30
    if (parent == null) {
Ian Hickson's avatar
Ian Hickson committed
31
      return '${objectRuntimeType(this, 'TestScrollPhysics')}($name)';
32
    }
Ian Hickson's avatar
Ian Hickson committed
33
    return '${objectRuntimeType(this, 'TestScrollPhysics')}($name) -> $parent';
34 35 36 37 38 39
  }
}


void main() {
  test('ScrollPhysics applyTo()', () {
40 41 42 43 44
    const TestScrollPhysics a = TestScrollPhysics(name: 'a');
    const TestScrollPhysics b = TestScrollPhysics(name: 'b');
    const TestScrollPhysics c = TestScrollPhysics(name: 'c');
    const TestScrollPhysics d = TestScrollPhysics(name: 'd');
    const TestScrollPhysics e = TestScrollPhysics(name: 'e');
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

    expect(a.parent, null);
    expect(b.parent, null);
    expect(c.parent, null);

    final TestScrollPhysics ab = a.applyTo(b);
    expect(ab.names, 'a b');

    final TestScrollPhysics abc = ab.applyTo(c);
    expect(abc.names, 'a b c');

    final TestScrollPhysics de = d.applyTo(e);
    expect(de.names, 'd e');

    final TestScrollPhysics abcde = abc.applyTo(de);
    expect(abcde.names, 'a b c d e');
  });

  test('ScrollPhysics subclasses applyTo()', () {
64 65 66 67 68
    const ScrollPhysics bounce = BouncingScrollPhysics();
    const ScrollPhysics clamp = ClampingScrollPhysics();
    const ScrollPhysics never = NeverScrollableScrollPhysics();
    const ScrollPhysics always = AlwaysScrollableScrollPhysics();
    const ScrollPhysics page = PageScrollPhysics();
69
    const ScrollPhysics bounceDesktop = BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast);
70

71
    String types(ScrollPhysics? value) => value!.parent == null ? '${value.runtimeType}' : '${value.runtimeType} ${types(value.parent)}';
72

73 74 75 76
    expect(
      types(bounce.applyTo(clamp.applyTo(never.applyTo(always.applyTo(page))))),
      'BouncingScrollPhysics ClampingScrollPhysics NeverScrollableScrollPhysics AlwaysScrollableScrollPhysics PageScrollPhysics',
    );
77

78 79 80 81
    expect(
      types(clamp.applyTo(never.applyTo(always.applyTo(page.applyTo(bounce))))),
      'ClampingScrollPhysics NeverScrollableScrollPhysics AlwaysScrollableScrollPhysics PageScrollPhysics BouncingScrollPhysics',
    );
82

83 84 85 86
    expect(
      types(never.applyTo(always.applyTo(page.applyTo(bounce.applyTo(clamp))))),
      'NeverScrollableScrollPhysics AlwaysScrollableScrollPhysics PageScrollPhysics BouncingScrollPhysics ClampingScrollPhysics',
    );
87

88 89 90 91
    expect(
      types(always.applyTo(page.applyTo(bounce.applyTo(clamp.applyTo(never))))),
      'AlwaysScrollableScrollPhysics PageScrollPhysics BouncingScrollPhysics ClampingScrollPhysics NeverScrollableScrollPhysics',
    );
92

93 94 95 96
    expect(
      types(page.applyTo(bounce.applyTo(clamp.applyTo(never.applyTo(always))))),
      'PageScrollPhysics BouncingScrollPhysics ClampingScrollPhysics NeverScrollableScrollPhysics AlwaysScrollableScrollPhysics',
    );
97 98 99 100 101

    expect(
      bounceDesktop.applyTo(always),
      (BouncingScrollPhysics x) => x.decelerationRate == ScrollDecelerationRate.fast
    );
102
  });
103

104
  test("ScrollPhysics scrolling subclasses - Creating the simulation doesn't alter the velocity for time 0", () {
105 106 107 108 109 110
    final ScrollMetrics position = FixedScrollMetrics(
      minScrollExtent: 0.0,
      maxScrollExtent: 100.0,
      pixels: 20.0,
      viewportDimension: 500.0,
      axisDirection: AxisDirection.down,
111
      devicePixelRatio: 3.0,
112 113 114 115
    );

    const BouncingScrollPhysics bounce = BouncingScrollPhysics();
    const ClampingScrollPhysics clamp = ClampingScrollPhysics();
116
    const PageScrollPhysics page = PageScrollPhysics();
117 118

    // Calls to createBallisticSimulation may happen on every frame (i.e. when the maxScrollExtent changes)
119 120 121 122
    // Changing velocity for time 0 may cause a sudden, unwanted damping/speedup effect
    expect(bounce.createBallisticSimulation(position, 1000)!.dx(0), moreOrLessEquals(1000));
    expect(clamp.createBallisticSimulation(position, 1000)!.dx(0), moreOrLessEquals(1000));
    expect(page.createBallisticSimulation(position, 1000)!.dx(0), moreOrLessEquals(1000));
123 124
  });

125
  group('BouncingScrollPhysics test', () {
126
    late BouncingScrollPhysics physicsUnderTest;
127 128 129 130 131 132

    setUp(() {
      physicsUnderTest = const BouncingScrollPhysics();
    });

    test('overscroll is progressively harder', () {
133
      final ScrollMetrics lessOverscrolledPosition = FixedScrollMetrics(
134 135 136 137 138 139
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: -20.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
        devicePixelRatio: 3.0,
140 141
      );

142
      final ScrollMetrics moreOverscrolledPosition = FixedScrollMetrics(
143 144 145 146 147
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: -40.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
148
        devicePixelRatio: 3.0,
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
      );

      final double lessOverscrollApplied =
          physicsUnderTest.applyPhysicsToUserOffset(lessOverscrolledPosition, 10.0);

      final double moreOverscrollApplied =
          physicsUnderTest.applyPhysicsToUserOffset(moreOverscrolledPosition, 10.0);

      expect(lessOverscrollApplied, greaterThan(1.0));
      expect(lessOverscrollApplied, lessThan(20.0));

      expect(moreOverscrollApplied, greaterThan(1.0));
      expect(moreOverscrollApplied, lessThan(20.0));

      // Scrolling from a more overscrolled position meets more resistance.
164 165 166 167
      expect(
        lessOverscrollApplied.abs(),
        greaterThan(moreOverscrollApplied.abs()),
      );
168 169 170
    });

    test('easing an overscroll still has resistance', () {
171
      final ScrollMetrics overscrolledPosition = FixedScrollMetrics(
172 173 174 175 176
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: -20.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
177
        devicePixelRatio: 3.0,
178 179 180 181 182 183 184 185 186 187
      );

      final double easingApplied =
          physicsUnderTest.applyPhysicsToUserOffset(overscrolledPosition, -10.0);

      expect(easingApplied, lessThan(-1.0));
      expect(easingApplied, greaterThan(-10.0));
    });

    test('no resistance when not overscrolled', () {
188
      final ScrollMetrics scrollPosition = FixedScrollMetrics(
189 190 191 192 193
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: 300.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
194
        devicePixelRatio: 3.0,
195 196
      );

197 198 199 200 201 202 203 204
      expect(
        physicsUnderTest.applyPhysicsToUserOffset(scrollPosition, 10.0),
        10.0,
      );
      expect(
        physicsUnderTest.applyPhysicsToUserOffset(scrollPosition, -10.0),
        -10.0,
      );
205 206 207
    });

    test('easing an overscroll meets less resistance than tensioning', () {
208
      final ScrollMetrics overscrolledPosition = FixedScrollMetrics(
209 210 211 212 213
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: -20.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
214
        devicePixelRatio: 3.0,
215 216 217 218 219 220 221 222 223
      );

      final double easingApplied =
          physicsUnderTest.applyPhysicsToUserOffset(overscrolledPosition, -10.0);
      final double tensioningApplied =
          physicsUnderTest.applyPhysicsToUserOffset(overscrolledPosition, 10.0);

      expect(easingApplied.abs(), greaterThan(tensioningApplied.abs()));
    });
224

225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
    test('no easing resistance for ScrollDecelerationRate.fast', () {
      const BouncingScrollPhysics desktop = BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast);
      final ScrollMetrics overscrolledPosition = FixedScrollMetrics(
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: -20.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
        devicePixelRatio: 3.0,
      );

      final double easingApplied =
          desktop.applyPhysicsToUserOffset(overscrolledPosition, -10.0);
      final double tensioningApplied =
          desktop.applyPhysicsToUserOffset(overscrolledPosition, 10.0);

      expect(tensioningApplied.abs(), lessThan(easingApplied.abs()));
      expect(easingApplied, -10);
    });

245
    test('overscroll a small list and a big list works the same way', () {
246
      final ScrollMetrics smallListOverscrolledPosition = FixedScrollMetrics(
247 248 249 250 251 252
        minScrollExtent: 0.0,
        maxScrollExtent: 10.0,
        pixels: -20.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
        devicePixelRatio: 3.0,
253 254
      );

255
      final ScrollMetrics bigListOverscrolledPosition = FixedScrollMetrics(
256 257 258 259 260
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: -20.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
261
        devicePixelRatio: 3.0,
262 263 264 265 266 267 268 269 270 271 272 273 274
      );

      final double smallListOverscrollApplied =
          physicsUnderTest.applyPhysicsToUserOffset(smallListOverscrolledPosition, 10.0);

      final double bigListOverscrollApplied =
          physicsUnderTest.applyPhysicsToUserOffset(bigListOverscrolledPosition, 10.0);

      expect(smallListOverscrollApplied, equals(bigListOverscrollApplied));

      expect(smallListOverscrollApplied, greaterThan(1.0));
      expect(smallListOverscrollApplied, lessThan(20.0));
    });
275 276 277 278 279 280 281 282 283 284 285 286 287 288

    test('frictionFactor', () {
      const BouncingScrollPhysics mobile = BouncingScrollPhysics();
      const BouncingScrollPhysics desktop = BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast);

      expect(desktop.frictionFactor(0), 0.26);
      expect(mobile.frictionFactor(0), 0.52);

      expect(desktop.frictionFactor(0.4), moreOrLessEquals(0.0936));
      expect(mobile.frictionFactor(0.4), moreOrLessEquals(0.1872));

      expect(desktop.frictionFactor(0.8), moreOrLessEquals(0.0104));
      expect(mobile.frictionFactor(0.8), moreOrLessEquals(0.0208));
    });
289
  });
290 291 292 293 294 295 296 297 298 299

  test('ClampingScrollPhysics assertion test', () {
    const ClampingScrollPhysics physics = ClampingScrollPhysics();
    const double pixels = 500;
    final ScrollMetrics position = FixedScrollMetrics(
      pixels: pixels,
      minScrollExtent: 0,
      maxScrollExtent: 1000,
      viewportDimension: 0,
      axisDirection: AxisDirection.down,
300
      devicePixelRatio: 3.0,
301 302
    );
    expect(position.pixels, pixels);
303
    late FlutterError error;
304 305 306 307 308 309 310
    try {
      physics.applyBoundaryConditions(position, pixels);
    } on FlutterError catch (e) {
      error = e;
    } finally {
      expect(error, isNotNull);
      expect(error.diagnostics.length, 4);
Dan Field's avatar
Dan Field committed
311
      expect(error.diagnostics[2], isA<DiagnosticsProperty<ScrollPhysics>>());
312 313
      expect(error.diagnostics[2].style, DiagnosticsTreeStyle.errorProperty);
      expect(error.diagnostics[2].value, physics);
Dan Field's avatar
Dan Field committed
314
      expect(error.diagnostics[3], isA<DiagnosticsProperty<ScrollMetrics>>());
315 316 317 318 319
      expect(error.diagnostics[3].style, DiagnosticsTreeStyle.errorProperty);
      expect(error.diagnostics[3].value, position);
      // RegExp matcher is required here due to flutter web and flutter mobile generating
      // slightly different floating point numbers
      // in Flutter web 0.0 sometimes just appears as 0. or 0
320 321
      expect(
        error.toStringDeep(),
322 323
        matches(RegExp(
          r'''
324
FlutterError
325 326 327 328 329 330 331 332 333 334 335 336
   ClampingScrollPhysics\.applyBoundaryConditions\(\) was called
   redundantly\.
   The proposed new position\, 500(\.\d*)?, is exactly equal to the current
   position of the given FixedScrollMetrics, 500(\.\d*)?\.
   The applyBoundaryConditions method should only be called when the
   value is going to actually change the pixels, otherwise it is
   redundant\.
   The physics object in question was\:
     ClampingScrollPhysics
   The position object in question was\:
     FixedScrollMetrics\(500(\.\d*)?..\[0(\.\d*)?\]..500(\.\d*)?\)
''',
337
          multiLine: true,
338
        )),
339
      );
340 341
    }
  });
342

343
  testWidgetsWithLeakTracking('PageScrollPhysics work with NestedScrollView', (WidgetTester tester) async {
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
    // Regression test for: https://github.com/flutter/flutter/issues/47850
    await tester.pumpWidget(Material(
      child: Directionality(
        textDirection: TextDirection.ltr,
        child: NestedScrollView(
          physics: const PageScrollPhysics(),
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              SliverToBoxAdapter(child: Container(height: 300, color: Colors.blue)),
            ];
          },
          body: ListView.builder(
            itemBuilder: (BuildContext context, int index) {
              return Text('Index $index');
            },
            itemCount: 100,
          ),
        ),
362
      ),
363 364 365
    ));
    await tester.fling(find.text('Index 2'), const Offset(0.0, -300.0), 10000.0);
  });
366
}