Unverified Commit 3b9b5ace authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

showOnScreen Improvements (#18252)

parent c53245c6
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4; import 'package:vector_math/vector_math_64.dart' show Matrix4;
...@@ -614,27 +615,50 @@ class RenderListWheelViewport ...@@ -614,27 +615,50 @@ class RenderListWheelViewport
} }
@override @override
double getOffsetToReveal(RenderObject target, double alignment) { RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) {
final ListWheelParentData parentData = target.parentData; // `target` is only fully revealed when in the selected/center position. Therefore,
final double centerPosition = parentData.offset.dy; // this method always returns the offset that shows `target` in the center position,
// which is the same offset for all `alignment` values.
if (alignment < 0.5) {
return centerPosition + _topScrollMarginExtent * alignment * 2.0; rect ??= target.paintBounds;
} else if (alignment > 0.5) {
return centerPosition - _topScrollMarginExtent * (alignment - 0.5) * 2.0; // `child` will be the last RenderObject before the viewport when walking up from `target`.
} else { RenderObject child = target;
return centerPosition; while (child.parent != this)
} child = child.parent;
final ListWheelParentData parentData = child.parentData;
final double targetOffset = parentData.offset.dy; // the so-called "centerPosition"
final Matrix4 transform = target.getTransformTo(this);
final Rect bounds = MatrixUtils.transformRect(transform, rect);
final Rect targetRect = bounds.translate(0.0, (size.height - itemExtent) / 2);
return new RevealedOffset(offset: targetOffset, rect: targetRect);
} }
@override @override
void showOnScreen([RenderObject child]) { void showOnScreen({
if (child != null) { RenderObject descendant,
// Shows the child in the selected/center position. Rect rect,
offset.jumpTo(getOffsetToReveal(child, 0.5)); Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
if (descendant != null) {
// Shows the descendant in the selected/center position.
final RevealedOffset revealedOffset = getOffsetToReveal(descendant, 0.5, rect: rect);
if (duration == Duration.zero) {
offset.jumpTo(revealedOffset.offset);
} else {
offset.animateTo(revealedOffset.offset, duration: duration, curve: curve);
}
rect = revealedOffset.rect;
} }
// Make sure the viewport itself is on screen. super.showOnScreen(
super.showOnScreen(); rect: rect,
duration: duration,
curve: curve,
);
} }
} }
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:developer'; import 'dart:developer';
import 'dart:ui' as ui show PictureRecorder; import 'dart:ui' as ui show PictureRecorder;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
...@@ -2036,6 +2037,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2036,6 +2037,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// An estimate of the bounds within which this render object will paint. /// An estimate of the bounds within which this render object will paint.
/// Useful for debugging flags such as [debugPaintLayerBordersEnabled]. /// Useful for debugging flags such as [debugPaintLayerBordersEnabled].
///
/// These are also the bounds used by [showOnScreen] to make a [RenderObject]
/// visible on screen.
Rect get paintBounds; Rect get paintBounds;
/// Override this method to paint debugging information. /// Override this method to paint debugging information.
...@@ -2570,14 +2574,35 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2570,14 +2574,35 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
@override @override
List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[]; List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[];
/// Attempt to make this or a descendant RenderObject visible on screen. /// Attempt to make (a portion of) this or a descendant [RenderObject] visible
/// /// on screen.
/// If [child] is provided, that [RenderObject] is made visible. If [child] is ///
/// omitted, this [RenderObject] is made visible. /// If `descendant` is provided, that [RenderObject] is made visible. If
void showOnScreen([RenderObject child]) { /// `descendant` is omitted, this [RenderObject] is made visible.
///
/// The optional `rect` parameter describes which area of that [RenderObject]
/// should be shown on screen. If `rect` is null, the entire
/// [RenderObject] (as defined by its [paintBounds]) will be revealed. The
/// `rect` parameter is interpreted relative to the coordinate system of
/// `descendant` if that argument is provided and relative to this
/// [RenderObject] otherwise.
///
/// The `duration` parameter can be set to a non-zero value to bring the
/// target object on screen in an animation defined by `curve`.
void showOnScreen({
RenderObject descendant,
Rect rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
if (parent is RenderObject) { if (parent is RenderObject) {
final RenderObject renderParent = parent; final RenderObject renderParent = parent;
renderParent.showOnScreen(child ?? this); renderParent.showOnScreen(
descendant: descendant ?? this,
rect: rect,
duration: duration,
curve: curve,
);
} }
} }
} }
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
/// The direction of a scroll, relative to the positive scroll offset axis given /// The direction of a scroll, relative to the positive scroll offset axis given
...@@ -157,7 +160,7 @@ abstract class ViewportOffset extends ChangeNotifier { ...@@ -157,7 +160,7 @@ abstract class ViewportOffset extends ChangeNotifier {
/// [jumpTo] applies the change immediately and notifies its listeners. /// [jumpTo] applies the change immediately and notifies its listeners.
void correctBy(double correction); void correctBy(double correction);
/// Jumps the scroll position from its current value to the given value, /// Jumps [pixels] from its current value to the given value,
/// without animation, and without checking if the new value is in range. /// without animation, and without checking if the new value is in range.
/// ///
/// See also: /// See also:
...@@ -166,6 +169,18 @@ abstract class ViewportOffset extends ChangeNotifier { ...@@ -166,6 +169,18 @@ abstract class ViewportOffset extends ChangeNotifier {
/// and that defers the notification of its listeners until after layout. /// and that defers the notification of its listeners until after layout.
void jumpTo(double pixels); void jumpTo(double pixels);
/// Animates [pixels] from its current value to the given value.
///
/// The returned [Future] will complete when the animation ends, whether it
/// completed successfully or whether it was interrupted prematurely.
///
/// The duration must not be zero. To jump to a particular value without an
/// animation, use [jumpTo].
Future<Null> animateTo(double to, {
@required Duration duration,
@required Curve curve,
});
/// The direction in which the user is trying to change [pixels], relative to /// The direction in which the user is trying to change [pixels], relative to
/// the viewport's [RenderViewport.axisDirection]. /// the viewport's [RenderViewport.axisDirection].
/// ///
...@@ -227,6 +242,12 @@ class _FixedViewportOffset extends ViewportOffset { ...@@ -227,6 +242,12 @@ class _FixedViewportOffset extends ViewportOffset {
// Do nothing, viewport is fixed. // Do nothing, viewport is fixed.
} }
@override
Future<Null> animateTo(double to, {
@required Duration duration,
@required Curve curve,
}) async => null;
@override @override
ScrollDirection get userScrollDirection => ScrollDirection.idle; ScrollDirection get userScrollDirection => ScrollDirection.idle;
} }
...@@ -497,7 +497,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -497,7 +497,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object); final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
assert(viewport != null); assert(viewport != null);
final double target = viewport.getOffsetToReveal(object, alignment).clamp(minScrollExtent, maxScrollExtent); final double target = viewport.getOffsetToReveal(object, alignment).offset.clamp(minScrollExtent, maxScrollExtent);
if (target == pixels) if (target == pixels)
return new Future<Null>.value(); return new Future<Null>.value();
...@@ -544,6 +544,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -544,6 +544,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// animation, use [jumpTo]. /// animation, use [jumpTo].
/// ///
/// The animation is typically handled by an [DrivenScrollActivity]. /// The animation is typically handled by an [DrivenScrollActivity].
@override
Future<Null> animateTo(double to, { Future<Null> animateTo(double to, {
@required Duration duration, @required Duration duration,
@required Curve curve, @required Curve curve,
......
...@@ -484,17 +484,19 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -484,17 +484,19 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
} }
Offset get _paintOffset { Offset get _paintOffset => _paintOffsetForPosition(offset.pixels);
Offset _paintOffsetForPosition(double position) {
assert(axisDirection != null); assert(axisDirection != null);
switch (axisDirection) { switch (axisDirection) {
case AxisDirection.up: case AxisDirection.up:
return new Offset(0.0, _offset.pixels - child.size.height + size.height); return new Offset(0.0, position - child.size.height + size.height);
case AxisDirection.down: case AxisDirection.down:
return new Offset(0.0, -_offset.pixels); return new Offset(0.0, -position);
case AxisDirection.left: case AxisDirection.left:
return new Offset(_offset.pixels - child.size.width + size.width, 0.0); return new Offset(position - child.size.width + size.width, 0.0);
case AxisDirection.right: case AxisDirection.right:
return new Offset(-_offset.pixels, 0.0); return new Offset(-position, 0.0);
} }
return null; return null;
} }
...@@ -544,13 +546,14 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -544,13 +546,14 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
} }
@override @override
double getOffsetToReveal(RenderObject target, double alignment) { RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) {
rect ??= target.paintBounds;
if (target is! RenderBox) if (target is! RenderBox)
return offset.pixels; return new RevealedOffset(offset: offset.pixels, rect: rect);
final RenderBox targetBox = target; final RenderBox targetBox = target;
final Matrix4 transform = targetBox.getTransformTo(this); final Matrix4 transform = targetBox.getTransformTo(this);
final Rect bounds = MatrixUtils.transformRect(transform, targetBox.paintBounds); final Rect bounds = MatrixUtils.transformRect(transform, rect);
final Size contentSize = child.size; final Size contentSize = child.size;
double leadingScrollOffset; double leadingScrollOffset;
...@@ -581,14 +584,31 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -581,14 +584,31 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
break; break;
} }
return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment; final double targetOffset = leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
final Rect targetRect = bounds.shift(_paintOffsetForPosition(targetOffset));
return new RevealedOffset(offset: targetOffset, rect: targetRect);
} }
@override @override
void showOnScreen([RenderObject child]) { void showOnScreen({
RenderViewportBase.showInViewport(child: child, viewport: this, offset: offset); RenderObject descendant,
// Make sure the viewport itself is on screen. Rect rect,
super.showOnScreen(); Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
final Rect newRect = RenderViewportBase.showInViewport(
descendant: descendant,
viewport: this,
offset: offset,
rect: rect,
duration: duration,
curve: curve,
);
super.showOnScreen(
rect: newRect,
duration: duration,
curve: curve,
);
} }
@override @override
......
This diff is collapsed.
...@@ -743,4 +743,127 @@ void main() { ...@@ -743,4 +743,127 @@ void main() {
debugDefaultTargetPlatformOverride = null; debugDefaultTargetPlatformOverride = null;
}); });
}); });
testWidgets('ListWheelScrollView getOffsetToReveal', (WidgetTester tester) async {
List<Widget> outerChildren;
final List<Widget> innerChildren = new List<Widget>(10);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 500.0,
width: 300.0,
child: new ListWheelScrollView(
controller: new ScrollController(initialScrollOffset: 300.0),
itemExtent: 100.0,
children: outerChildren = new List<Widget>.generate(10, (int i) {
return new Container(
child: new Center(
child: innerChildren[i] = new Container(
height: 50.0,
width: 50.0,
child: new Text('Item $i'),
),
),
);
}),
),
),
),
),
);
final RenderListWheelViewport viewport = tester.allRenderObjects.firstWhere((RenderObject r) => r is RenderListWheelViewport);
// direct child of viewport
RenderObject target = tester.renderObject(find.byWidget(outerChildren[5]));
RevealedOffset revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 200.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(0.0, 200.0, 300.0, 100.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 240.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(40.0, 240.0, 10.0, 10.0));
// descendant of viewport, not direct child
target = tester.renderObject(find.byWidget(innerChildren[5]));
revealed = viewport.getOffsetToReveal(target, 0.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(125.0, 225.0, 50.0, 50.0));
revealed = viewport.getOffsetToReveal(target, 1.0);
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(125.0, 225.0, 50.0, 50.0));
revealed = viewport.getOffsetToReveal(target, 0.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(165.0, 265.0, 10.0, 10.0));
revealed = viewport.getOffsetToReveal(target, 1.0, rect: new Rect.fromLTWH(40.0, 40.0, 10.0, 10.0));
expect(revealed.offset, 500.0);
expect(revealed.rect, new Rect.fromLTWH(165.0, 265.0, 10.0, 10.0));
});
testWidgets('ListWheelScrollView showOnScreen', (WidgetTester tester) async {
List<Widget> outerChildren;
final List<Widget> innerChildren = new List<Widget>(10);
ScrollController controller;
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Center(
child: Container(
height: 500.0,
width: 300.0,
child: new ListWheelScrollView(
controller: controller = new ScrollController(initialScrollOffset: 300.0),
itemExtent: 100.0,
children:
outerChildren = new List<Widget>.generate(10, (int i) {
return new Container(
child: new Center(
child: innerChildren[i] = new Container(
height: 50.0,
width: 50.0,
child: new Text('Item $i'),
),
),
);
}),
),
),
),
),
);
expect(controller.offset, 300.0);
tester.renderObject(find.byWidget(outerChildren[5])).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 500.0);
tester.renderObject(find.byWidget(innerChildren[9])).showOnScreen();
await tester.pumpAndSettle();
expect(controller.offset, 900.0);
tester.renderObject(find.byWidget(outerChildren[7])).showOnScreen(duration: const Duration(seconds: 2));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(tester.hasRunningAnimations, isTrue);
expect(controller.offset, lessThan(900.0));
expect(controller.offset, greaterThan(700.0));
await tester.pumpAndSettle();
expect(controller.offset, 700.0);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment