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

showOnScreen Improvements (#18252)

parent c53245c6
......@@ -4,6 +4,7 @@
import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart' show Matrix4;
......@@ -614,27 +615,50 @@ class RenderListWheelViewport
}
@override
double getOffsetToReveal(RenderObject target, double alignment) {
final ListWheelParentData parentData = target.parentData;
final double centerPosition = parentData.offset.dy;
if (alignment < 0.5) {
return centerPosition + _topScrollMarginExtent * alignment * 2.0;
} else if (alignment > 0.5) {
return centerPosition - _topScrollMarginExtent * (alignment - 0.5) * 2.0;
} else {
return centerPosition;
}
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) {
// `target` is only fully revealed when in the selected/center position. Therefore,
// this method always returns the offset that shows `target` in the center position,
// which is the same offset for all `alignment` values.
rect ??= target.paintBounds;
// `child` will be the last RenderObject before the viewport when walking up from `target`.
RenderObject child = target;
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
void showOnScreen([RenderObject child]) {
if (child != null) {
// Shows the child in the selected/center position.
offset.jumpTo(getOffsetToReveal(child, 0.5));
void showOnScreen({
RenderObject descendant,
Rect rect,
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 @@
import 'dart:developer';
import 'dart:ui' as ui show PictureRecorder;
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/painting.dart';
......@@ -2036,6 +2037,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
/// An estimate of the bounds within which this render object will paint.
/// 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;
/// Override this method to paint debugging information.
......@@ -2570,14 +2574,35 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
@override
List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[];
/// Attempt to make 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.
void showOnScreen([RenderObject child]) {
/// Attempt to make (a portion of) this or a descendant [RenderObject] visible
/// on screen.
///
/// If `descendant` is provided, that [RenderObject] is made visible. If
/// `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) {
final RenderObject renderParent = parent;
renderParent.showOnScreen(child ?? this);
renderParent.showOnScreen(
descendant: descendant ?? this,
rect: rect,
duration: duration,
curve: curve,
);
}
}
}
......
......@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart';
/// The direction of a scroll, relative to the positive scroll offset axis given
......@@ -157,7 +160,7 @@ abstract class ViewportOffset extends ChangeNotifier {
/// [jumpTo] applies the change immediately and notifies its listeners.
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.
///
/// See also:
......@@ -166,6 +169,18 @@ abstract class ViewportOffset extends ChangeNotifier {
/// and that defers the notification of its listeners until after layout.
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 viewport's [RenderViewport.axisDirection].
///
......@@ -227,6 +242,12 @@ class _FixedViewportOffset extends ViewportOffset {
// Do nothing, viewport is fixed.
}
@override
Future<Null> animateTo(double to, {
@required Duration duration,
@required Curve curve,
}) async => null;
@override
ScrollDirection get userScrollDirection => ScrollDirection.idle;
}
......@@ -497,7 +497,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
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)
return new Future<Null>.value();
......@@ -544,6 +544,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// animation, use [jumpTo].
///
/// The animation is typically handled by an [DrivenScrollActivity].
@override
Future<Null> animateTo(double to, {
@required Duration duration,
@required Curve curve,
......
......@@ -484,17 +484,19 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
}
Offset get _paintOffset {
Offset get _paintOffset => _paintOffsetForPosition(offset.pixels);
Offset _paintOffsetForPosition(double position) {
assert(axisDirection != null);
switch (axisDirection) {
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:
return new Offset(0.0, -_offset.pixels);
return new Offset(0.0, -position);
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:
return new Offset(-_offset.pixels, 0.0);
return new Offset(-position, 0.0);
}
return null;
}
......@@ -544,13 +546,14 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
}
@override
double getOffsetToReveal(RenderObject target, double alignment) {
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, {Rect rect}) {
rect ??= target.paintBounds;
if (target is! RenderBox)
return offset.pixels;
return new RevealedOffset(offset: offset.pixels, rect: rect);
final RenderBox targetBox = target;
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;
double leadingScrollOffset;
......@@ -581,14 +584,31 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
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
void showOnScreen([RenderObject child]) {
RenderViewportBase.showInViewport(child: child, viewport: this, offset: offset);
// Make sure the viewport itself is on screen.
super.showOnScreen();
void showOnScreen({
RenderObject descendant,
Rect rect,
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
......
This diff is collapsed.
......@@ -743,4 +743,127 @@ void main() {
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