scroll_physics_test.dart 11.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 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

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

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

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

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

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

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

97
  test("ScrollPhysics scrolling subclasses - Creating the simulation doesn't alter the velocity for time 0", () {
98 99 100 101 102 103 104 105 106 107
    final ScrollMetrics position = FixedScrollMetrics(
      minScrollExtent: 0.0,
      maxScrollExtent: 100.0,
      pixels: 20.0,
      viewportDimension: 500.0,
      axisDirection: AxisDirection.down,
    );

    const BouncingScrollPhysics bounce = BouncingScrollPhysics();
    const ClampingScrollPhysics clamp = ClampingScrollPhysics();
108
    const PageScrollPhysics page = PageScrollPhysics();
109 110

    // Calls to createBallisticSimulation may happen on every frame (i.e. when the maxScrollExtent changes)
111 112 113 114
    // 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));
115 116
  });

117
  group('BouncingScrollPhysics test', () {
118
    late BouncingScrollPhysics physicsUnderTest;
119 120 121 122 123 124

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

    test('overscroll is progressively harder', () {
125
      final ScrollMetrics lessOverscrolledPosition = FixedScrollMetrics(
126 127 128 129 130 131 132
          minScrollExtent: 0.0,
          maxScrollExtent: 1000.0,
          pixels: -20.0,
          viewportDimension: 100.0,
          axisDirection: AxisDirection.down,
      );

133
      final ScrollMetrics moreOverscrolledPosition = FixedScrollMetrics(
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: -40.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
      );

      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.
154 155 156 157
      expect(
        lessOverscrollApplied.abs(),
        greaterThan(moreOverscrollApplied.abs()),
      );
158 159 160
    });

    test('easing an overscroll still has resistance', () {
161
      final ScrollMetrics overscrolledPosition = FixedScrollMetrics(
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: -20.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
      );

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

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

    test('no resistance when not overscrolled', () {
177
      final ScrollMetrics scrollPosition = FixedScrollMetrics(
178 179 180 181 182 183 184
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: 300.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
      );

185 186 187 188 189 190 191 192
      expect(
        physicsUnderTest.applyPhysicsToUserOffset(scrollPosition, 10.0),
        10.0,
      );
      expect(
        physicsUnderTest.applyPhysicsToUserOffset(scrollPosition, -10.0),
        -10.0,
      );
193 194 195
    });

    test('easing an overscroll meets less resistance than tensioning', () {
196
      final ScrollMetrics overscrolledPosition = FixedScrollMetrics(
197 198 199 200 201 202 203 204 205 206 207 208 209 210
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: -20.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
      );

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

      expect(easingApplied.abs(), greaterThan(tensioningApplied.abs()));
    });
211 212

    test('overscroll a small list and a big list works the same way', () {
213
      final ScrollMetrics smallListOverscrolledPosition = FixedScrollMetrics(
214 215 216 217 218 219 220
          minScrollExtent: 0.0,
          maxScrollExtent: 10.0,
          pixels: -20.0,
          viewportDimension: 100.0,
          axisDirection: AxisDirection.down,
      );

221
      final ScrollMetrics bigListOverscrolledPosition = FixedScrollMetrics(
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
        minScrollExtent: 0.0,
        maxScrollExtent: 1000.0,
        pixels: -20.0,
        viewportDimension: 100.0,
        axisDirection: AxisDirection.down,
      );

      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));
    });
240
  });
241 242 243 244 245 246 247 248 249 250 251 252

  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,
    );
    expect(position.pixels, pixels);
253
    late FlutterError error;
254 255 256 257 258 259 260
    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
261
      expect(error.diagnostics[2], isA<DiagnosticsProperty<ScrollPhysics>>());
262 263
      expect(error.diagnostics[2].style, DiagnosticsTreeStyle.errorProperty);
      expect(error.diagnostics[2].value, physics);
Dan Field's avatar
Dan Field committed
264
      expect(error.diagnostics[3], isA<DiagnosticsProperty<ScrollMetrics>>());
265 266 267 268 269
      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
270 271
      expect(
        error.toStringDeep(),
272 273
        matches(RegExp(
          r'''
274
FlutterError
275 276 277 278 279 280 281 282 283 284 285 286
   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*)?\)
''',
287
          multiLine: true,
288
        )),
289
      );
290 291
    }
  });
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311

  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,
          ),
        ),
312
      ),
313 314 315
    ));
    await tester.fling(find.text('Index 2'), const Offset(0.0, -300.0), 10000.0);
  });
316
}