scroll_physics_test.dart 13 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 8 9
import 'package:flutter_test/flutter_test.dart';

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

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

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

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


void main() {
  test('ScrollPhysics applyTo()', () {
39 40 41 42 43
    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');
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62

    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()', () {
63 64 65 66 67
    const ScrollPhysics bounce = BouncingScrollPhysics();
    const ScrollPhysics clamp = ClampingScrollPhysics();
    const ScrollPhysics never = NeverScrollableScrollPhysics();
    const ScrollPhysics always = AlwaysScrollableScrollPhysics();
    const ScrollPhysics page = PageScrollPhysics();
68
    const ScrollPhysics bounceDesktop = BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast);
69

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

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

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

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

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

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

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

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

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

    // Calls to createBallisticSimulation may happen on every frame (i.e. when the maxScrollExtent changes)
118 119 120 121
    // 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));
122 123
  });

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

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

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

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

      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.
163 164 165 166
      expect(
        lessOverscrollApplied.abs(),
        greaterThan(moreOverscrollApplied.abs()),
      );
167 168 169
    });

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

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

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

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

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

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

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

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

224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
    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);
    });

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

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

      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));
    });
274 275 276 277 278 279 280 281 282 283 284 285 286 287

    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));
    });
288
  });
289 290 291 292 293 294 295 296 297 298

  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,
299
      devicePixelRatio: 3.0,
300 301
    );
    expect(position.pixels, pixels);
302
    late FlutterError error;
303 304 305 306 307 308 309
    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
310
      expect(error.diagnostics[2], isA<DiagnosticsProperty<ScrollPhysics>>());
311 312
      expect(error.diagnostics[2].style, DiagnosticsTreeStyle.errorProperty);
      expect(error.diagnostics[2].value, physics);
Dan Field's avatar
Dan Field committed
313
      expect(error.diagnostics[3], isA<DiagnosticsProperty<ScrollMetrics>>());
314 315 316 317 318
      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
319 320
      expect(
        error.toStringDeep(),
321 322
        matches(RegExp(
          r'''
323
FlutterError
324 325 326 327 328 329 330 331 332 333 334 335
   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*)?\)
''',
336
          multiLine: true,
337
        )),
338
      );
339 340
    }
  });
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360

  testWidgets('PageScrollPhysics work with NestedScrollView', (WidgetTester tester) async {
    // 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,
          ),
        ),
361
      ),
362 363 364
    ));
    await tester.fling(find.text('Index 2'), const Offset(0.0, -300.0), 10000.0);
  });
365
}