// Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; void main() { testWidgets('Viewport getOffsetToReveal - down', (WidgetTester tester) async { List<Widget> children; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: Container( height: 200.0, width: 300.0, child: ListView( controller: ScrollController(initialScrollOffset: 300.0), children: children = List<Widget>.generate(20, (int i) { return Container( height: 100.0, width: 300.0, child: Text('Tile $i'), ); }), ), ), ), ), ); final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport); final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false)); RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0); expect(revealed.offset, 500.0); expect(revealed.rect, Rect.fromLTWH(0.0, 0.0, 300.0, 100.0)); revealed = viewport.getOffsetToReveal(target, 1.0); expect(revealed.offset, 400.0); expect(revealed.rect, Rect.fromLTWH(0.0, 100.0, 300.0, 100.0)); revealed = viewport.getOffsetToReveal(target, 0.0, rect: Rect.fromLTWH(40.0, 40.0, 10.0, 10.0)); expect(revealed.offset, 540.0); expect(revealed.rect, Rect.fromLTWH(40.0, 0.0, 10.0, 10.0)); revealed = viewport.getOffsetToReveal(target, 1.0, rect: Rect.fromLTWH(40.0, 40.0, 10.0, 10.0)); expect(revealed.offset, 350.0); expect(revealed.rect, Rect.fromLTWH(40.0, 190.0, 10.0, 10.0)); }); testWidgets('Viewport getOffsetToReveal - right', (WidgetTester tester) async { List<Widget> children; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: Container( height: 300.0, width: 200.0, child: ListView( scrollDirection: Axis.horizontal, controller: ScrollController(initialScrollOffset: 300.0), children: children = List<Widget>.generate(20, (int i) { return Container( height: 300.0, width: 100.0, child: Text('Tile $i'), ); }), ), ), ), ), ); final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport); final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false)); RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0); expect(revealed.offset, 500.0); expect(revealed.rect, Rect.fromLTWH(0.0, 0.0, 100.0, 300.0)); revealed = viewport.getOffsetToReveal(target, 1.0); expect(revealed.offset, 400.0); expect(revealed.rect, Rect.fromLTWH(100.0, 0.0, 100.0, 300.0)); revealed = viewport.getOffsetToReveal(target, 0.0, rect: Rect.fromLTWH(40.0, 40.0, 10.0, 10.0)); expect(revealed.offset, 540.0); expect(revealed.rect, Rect.fromLTWH(0.0, 40.0, 10.0, 10.0)); revealed = viewport.getOffsetToReveal(target, 1.0, rect: Rect.fromLTWH(40.0, 40.0, 10.0, 10.0)); expect(revealed.offset, 350.0); expect(revealed.rect, Rect.fromLTWH(190.0, 40.0, 10.0, 10.0)); }); testWidgets('Viewport getOffsetToReveal - up', (WidgetTester tester) async { List<Widget> children; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: Container( height: 200.0, width: 300.0, child: ListView( controller: ScrollController(initialScrollOffset: 300.0), reverse: true, children: children = List<Widget>.generate(20, (int i) { return Container( height: 100.0, width: 300.0, child: Text('Tile $i'), ); }), ), ), ), ), ); final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport); final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false)); RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0); expect(revealed.offset, 500.0); expect(revealed.rect, Rect.fromLTWH(0.0, 100.0, 300.0, 100.0)); revealed = viewport.getOffsetToReveal(target, 1.0); expect(revealed.offset, 400.0); expect(revealed.rect, Rect.fromLTWH(0.0, 0.0, 300.0, 100.0)); revealed = viewport.getOffsetToReveal(target, 0.0, rect: Rect.fromLTWH(40.0, 40.0, 10.0, 10.0)); expect(revealed.offset, 550.0); expect(revealed.rect, Rect.fromLTWH(40.0, 190.0, 10.0, 10.0)); revealed = viewport.getOffsetToReveal(target, 1.0, rect: Rect.fromLTWH(40.0, 40.0, 10.0, 10.0)); expect(revealed.offset, 360.0); expect(revealed.rect, Rect.fromLTWH(40.0, 0.0, 10.0, 10.0)); }); testWidgets('Viewport getOffsetToReveal - left', (WidgetTester tester) async { List<Widget> children; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: Container( height: 300.0, width: 200.0, child: ListView( scrollDirection: Axis.horizontal, reverse: true, controller: ScrollController(initialScrollOffset: 300.0), children: children = List<Widget>.generate(20, (int i) { return Container( height: 300.0, width: 100.0, child: Text('Tile $i'), ); }), ), ), ), ), ); final RenderAbstractViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderAbstractViewport); final RenderObject target = tester.renderObject(find.byWidget(children[5], skipOffstage: false)); RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0); expect(revealed.offset, 500.0); expect(revealed.rect, Rect.fromLTWH(100.0, 0.0, 100.0, 300.0)); revealed = viewport.getOffsetToReveal(target, 1.0); expect(revealed.offset, 400.0); expect(revealed.rect, Rect.fromLTWH(0.0, 0.0, 100.0, 300.0)); revealed = viewport.getOffsetToReveal(target, 0.0, rect: Rect.fromLTWH(40.0, 40.0, 10.0, 10.0)); expect(revealed.offset, 550.0); expect(revealed.rect, Rect.fromLTWH(190.0, 40.0, 10.0, 10.0)); revealed = viewport.getOffsetToReveal(target, 1.0, rect: Rect.fromLTWH(40.0, 40.0, 10.0, 10.0)); expect(revealed.offset, 360.0); expect(revealed.rect, Rect.fromLTWH(0.0, 40.0, 10.0, 10.0)); }); testWidgets('Nested Viewports showOnScreen', (WidgetTester tester) async { final List<List<Widget>> children = List<List<Widget>>(10); final List<ScrollController> controllersX = List<ScrollController>.generate(10, (int i) => ScrollController(initialScrollOffset: 400.0)); final ScrollController controllerY = ScrollController(initialScrollOffset: 400.0); /// Builds a gird: /// /// <- x -> /// 0 1 2 3 4 5 6 7 8 9 /// 0 c c c c c c c c c c /// 1 c c c c c c c c c c /// 2 c c c c c c c c c c /// 3 c c c c c c c c c c y /// 4 c c c c v v c c c c /// 5 c c c c v v c c c c /// 6 c c c c c c c c c c /// 7 c c c c c c c c c c /// 8 c c c c c c c c c c /// 9 c c c c c c c c c c /// /// Each c is a 100x100 container, v are containers visible in initial /// viewport. await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: Container( height: 200.0, width: 200.0, child: ListView( controller: controllerY, children: List<Widget>.generate(10, (int y) { return Container( height: 100.0, child: ListView( scrollDirection: Axis.horizontal, controller: controllersX[y], children: children[y] = List<Widget>.generate(10, (int x) { return Container( height: 100.0, width: 100.0, child: Text('$x,$y'), ); }), ), ); }), ), ), ), ), ); // Already in viewport tester.renderObject(find.byWidget(children[4][4], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controllersX[4].offset, 400.0); expect(controllerY.offset, 400.0); controllersX[4].jumpTo(400.0); controllerY.jumpTo(400.0); await tester.pumpAndSettle(); // Above viewport tester.renderObject(find.byWidget(children[3][4], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controllersX[3].offset, 400.0); expect(controllerY.offset, 300.0); controllersX[3].jumpTo(400.0); controllerY.jumpTo(400.0); await tester.pumpAndSettle(); // Below viewport tester.renderObject(find.byWidget(children[6][4], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controllersX[6].offset, 400.0); expect(controllerY.offset, 500.0); controllersX[6].jumpTo(400.0); controllerY.jumpTo(400.0); await tester.pumpAndSettle(); // Left of viewport tester.renderObject(find.byWidget(children[4][3], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controllersX[4].offset, 300.0); expect(controllerY.offset, 400.0); controllersX[4].jumpTo(400.0); controllerY.jumpTo(400.0); await tester.pumpAndSettle(); // Right of viewport tester.renderObject(find.byWidget(children[4][6], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controllersX[4].offset, 500.0); expect(controllerY.offset, 400.0); controllersX[4].jumpTo(400.0); controllerY.jumpTo(400.0); await tester.pumpAndSettle(); // Above and left of viewport tester.renderObject(find.byWidget(children[3][3], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controllersX[3].offset, 300.0); expect(controllerY.offset, 300.0); controllersX[3].jumpTo(400.0); controllerY.jumpTo(400.0); await tester.pumpAndSettle(); // Below and left of viewport tester.renderObject(find.byWidget(children[6][3], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controllersX[6].offset, 300.0); expect(controllerY.offset, 500.0); controllersX[6].jumpTo(400.0); controllerY.jumpTo(400.0); await tester.pumpAndSettle(); // Above and right of viewport tester.renderObject(find.byWidget(children[3][6], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controllersX[3].offset, 500.0); expect(controllerY.offset, 300.0); controllersX[3].jumpTo(400.0); controllerY.jumpTo(400.0); await tester.pumpAndSettle(); // Below and right of viewport tester.renderObject(find.byWidget(children[6][6], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controllersX[6].offset, 500.0); expect(controllerY.offset, 500.0); controllersX[6].jumpTo(400.0); controllerY.jumpTo(400.0); await tester.pumpAndSettle(); // Below and right of viewport with animations tester.renderObject(find.byWidget(children[6][6], skipOffstage: false)).showOnScreen(duration: const Duration(seconds: 2)); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(tester.hasRunningAnimations, isTrue); expect(controllersX[6].offset, greaterThan(400.0)); expect(controllersX[6].offset, lessThan(500.0)); expect(controllerY.offset, greaterThan(400.0)); expect(controllerY.offset, lessThan(500.0)); await tester.pumpAndSettle(); expect(controllersX[6].offset, 500.0); expect(controllerY.offset, 500.0); }); group('Nested viewports (same orientation) showOnScreen', () { List<Widget> children; Future<Null> buildNestedScroller({WidgetTester tester, ScrollController inner, ScrollController outer}) { return tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: Container( height: 200.0, width: 300.0, child: ListView( controller: outer, children: <Widget>[ Container( height: 200.0, ), Container( height: 200.0, width: 300.0, child: ListView( controller: inner, children: children = List<Widget>.generate(10, (int i) { return Container( height: 100.0, width: 300.0, child: Text('$i'), ); }), ), ), Container( height: 200.0, ) ], ), ), ), ), ); } testWidgets('in view in inner, but not in outer', (WidgetTester tester) async { final ScrollController inner = ScrollController(); final ScrollController outer = ScrollController(); await buildNestedScroller( tester: tester, inner: inner, outer: outer, ); expect(outer.offset, 0.0); expect(inner.offset, 0.0); tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(inner.offset, 0.0); expect(outer.offset, 100.0); }); testWidgets('not in view of neither inner nor outer', (WidgetTester tester) async { final ScrollController inner = ScrollController(); final ScrollController outer = ScrollController(); await buildNestedScroller( tester: tester, inner: inner, outer: outer, ); expect(outer.offset, 0.0); expect(inner.offset, 0.0); tester.renderObject(find.byWidget(children[4], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(inner.offset, 300.0); expect(outer.offset, 200.0); }); testWidgets('in view in inner and outer', (WidgetTester tester) async { final ScrollController inner = ScrollController(initialScrollOffset: 200.0); final ScrollController outer = ScrollController(initialScrollOffset: 200.0); await buildNestedScroller( tester: tester, inner: inner, outer: outer, ); expect(outer.offset, 200.0); expect(inner.offset, 200.0); tester.renderObject(find.byWidget(children[2])).showOnScreen(); await tester.pumpAndSettle(); expect(outer.offset, 200.0); expect(inner.offset, 200.0); }); testWidgets('inner shown in outer, but item not visible', (WidgetTester tester) async { final ScrollController inner = ScrollController(initialScrollOffset: 200.0); final ScrollController outer = ScrollController(initialScrollOffset: 200.0); await buildNestedScroller( tester: tester, inner: inner, outer: outer, ); expect(outer.offset, 200.0); expect(inner.offset, 200.0); tester.renderObject(find.byWidget(children[5], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(outer.offset, 200.0); expect(inner.offset, 400.0); }); testWidgets('inner half shown in outer, item only visible in inner', (WidgetTester tester) async { final ScrollController inner = ScrollController(); final ScrollController outer = ScrollController(initialScrollOffset: 100.0); await buildNestedScroller( tester: tester, inner: inner, outer: outer, ); expect(outer.offset, 100.0); expect(inner.offset, 0.0); tester.renderObject(find.byWidget(children[1])).showOnScreen(); await tester.pumpAndSettle(); expect(outer.offset, 200.0); expect(inner.offset, 0.0); }); }); testWidgets('Viewport showOnScreen with objects larger than viewport', (WidgetTester tester) async { List<Widget> children; ScrollController controller; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Center( child: Container( height: 200.0, child: ListView( controller: controller = ScrollController(initialScrollOffset: 300.0), children: children = List<Widget>.generate(20, (int i) { return Container( height: 300.0, child: Text('Tile $i'), ); }), ), ), ), ), ); expect(controller.offset, 300.0); // Already aligned with leading edge, nothing happens. tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controller.offset, 300.0); // Above leading edge aligns trailing edges tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controller.offset, 100.0); // Below trailing edge aligns leading edges tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controller.offset, 300.0); controller.jumpTo(250.0); await tester.pumpAndSettle(); expect(controller.offset, 250.0); // Partly visible across leading edge aligns trailing edges tester.renderObject(find.byWidget(children[0], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controller.offset, 100.0); controller.jumpTo(150.0); await tester.pumpAndSettle(); expect(controller.offset, 150.0); // Partly visible across trailing edge aligns leading edges tester.renderObject(find.byWidget(children[1], skipOffstage: false)).showOnScreen(); await tester.pumpAndSettle(); expect(controller.offset, 300.0); }); }