Commit 7c8e504e authored by Adam Barth's avatar Adam Barth

Merge pull request #1337 from abarth/scroll_focus_into_view

Scroll focused input widgets into view
parents 869d13c1 dd5df79e
...@@ -145,13 +145,22 @@ class AnimationController extends Animation<double> ...@@ -145,13 +145,22 @@ class AnimationController extends Animation<double>
} }
Future animateTo(double target, { Duration duration, Curve curve: Curves.linear }) { Future animateTo(double target, { Duration duration, Curve curve: Curves.linear }) {
Duration remainingDuration = (duration ?? this.duration) * (target - _value).abs(); Duration simulationDuration = duration;
if (simulationDuration == null) {
double range = upperBound - lowerBound;
if (range.isFinite) {
double remainingFraction = (target - _value).abs() / range;
simulationDuration = this.duration * remainingFraction;
}
}
stop(); stop();
if (remainingDuration == Duration.ZERO) if (simulationDuration == Duration.ZERO) {
assert(value == target);
return new Future.value(); return new Future.value();
assert(remainingDuration > Duration.ZERO); }
assert(simulationDuration > Duration.ZERO);
assert(!isAnimating); assert(!isAnimating);
return _startSimulation(new _TweenSimulation(_value, target, remainingDuration, curve)); return _startSimulation(new _TweenSimulation(_value, target, simulationDuration, curve));
} }
Future _startSimulation(Simulation simulation) { Future _startSimulation(Simulation simulation) {
......
...@@ -366,7 +366,7 @@ class ScaffoldState extends State<Scaffold> { ...@@ -366,7 +366,7 @@ class ScaffoldState extends State<Scaffold> {
} }
Widget build(BuildContext context) { Widget build(BuildContext context) {
EdgeDims padding = MediaQuery.of(context).padding; EdgeDims padding = MediaQuery.of(context)?.padding ?? EdgeDims.zero;
if (_snackBars.length > 0) { if (_snackBars.length > 0) {
ModalRoute route = ModalRoute.of(context); ModalRoute route = ModalRoute.of(context);
......
...@@ -2,7 +2,12 @@ ...@@ -2,7 +2,12 @@
// 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 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'media_query.dart';
import 'scrollable.dart';
// _noFocusedScope is used by Focus to track the case where none of the Focus // _noFocusedScope is used by Focus to track the case where none of the Focus
// component's subscopes (e.g. dialogs) are focused. This is distinct from the // component's subscopes (e.g. dialogs) are focused. This is distinct from the
...@@ -130,10 +135,13 @@ class Focus extends StatefulComponent { ...@@ -130,10 +135,13 @@ class Focus extends StatefulComponent {
/// Don't call moveTo() from your build() functions, it's intended to be /// Don't call moveTo() from your build() functions, it's intended to be
/// called from event listeners, e.g. in response to a finger tap or tab key. /// called from event listeners, e.g. in response to a finger tap or tab key.
static void moveTo(GlobalKey key) { static void moveTo(GlobalKey key) {
assert(key.currentContext != null); BuildContext focusedContext = key.currentContext;
assert(focusedContext != null);
_FocusScope focusScope = key.currentContext.ancestorWidgetOfExactType(_FocusScope); _FocusScope focusScope = key.currentContext.ancestorWidgetOfExactType(_FocusScope);
if (focusScope != null) if (focusScope != null) {
focusScope.focusState._setFocusedWidget(key); focusScope.focusState._setFocusedWidget(key);
Scrollable.ensureVisible(focusedContext);
}
} }
/// Focuses a particular focus scope, identified by its GlobalKey. The widget /// Focuses a particular focus scope, identified by its GlobalKey. The widget
...@@ -239,7 +247,30 @@ class FocusState extends State<Focus> { ...@@ -239,7 +247,30 @@ class FocusState extends State<Focus> {
super.dispose(); super.dispose();
} }
Size _mediaSize;
EdgeDims _mediaPadding;
void _ensureVisibleIfFocused() {
if (!Focus._atScope(context))
return;
BuildContext focusedContext = _focusedWidget?.currentContext;
if (focusedContext == null)
return;
Scrollable.ensureVisible(focusedContext);
}
Widget build(BuildContext context) { Widget build(BuildContext context) {
MediaQueryData data = MediaQuery.of(context);
if (data != null) {
Size newMediaSize = data.size;
EdgeDims newMediaPadding = data.padding;
if (newMediaSize != _mediaSize || newMediaPadding != _mediaPadding) {
_mediaSize = newMediaSize;
_mediaPadding = newMediaPadding;
scheduleMicrotask(_ensureVisibleIfFocused);
}
}
return new _FocusScope( return new _FocusScope(
focusState: this, focusState: this,
scopeFocused: Focus._atScope(context), scopeFocused: Focus._atScope(context),
......
...@@ -64,7 +64,7 @@ abstract class Scrollable extends StatefulComponent { ...@@ -64,7 +64,7 @@ abstract class Scrollable extends StatefulComponent {
} }
/// Scrolls the closest enclosing scrollable to make the given context visible. /// Scrolls the closest enclosing scrollable to make the given context visible.
static Future ensureVisible(BuildContext context, { Duration duration, Curve curve }) { static Future ensureVisible(BuildContext context, { Duration duration, Curve curve: Curves.ease }) {
assert(context.findRenderObject() is RenderBox); assert(context.findRenderObject() is RenderBox);
// TODO(abarth): This function doesn't handle nested scrollable widgets. // TODO(abarth): This function doesn't handle nested scrollable widgets.
...@@ -80,20 +80,43 @@ abstract class Scrollable extends StatefulComponent { ...@@ -80,20 +80,43 @@ abstract class Scrollable extends StatefulComponent {
assert(scrollableBox.attached); assert(scrollableBox.attached);
Size scrollableSize = scrollableBox.size; Size scrollableSize = scrollableBox.size;
double scrollOffsetDelta; double targetMin;
double targetMax;
double scrollableMin;
double scrollableMax;
switch (scrollable.config.scrollDirection) { switch (scrollable.config.scrollDirection) {
case Axis.vertical: case Axis.vertical:
Point targetCenter = targetBox.localToGlobal(new Point(0.0, targetSize.height / 2.0)); targetMin = targetBox.localToGlobal(Point.origin).y;
Point scrollableCenter = scrollableBox.localToGlobal(new Point(0.0, scrollableSize.height / 2.0)); targetMax = targetBox.localToGlobal(new Point(0.0, targetSize.height)).y;
scrollOffsetDelta = targetCenter.y - scrollableCenter.y; scrollableMin = scrollableBox.localToGlobal(Point.origin).y;
scrollableMax = scrollableBox.localToGlobal(new Point(0.0, scrollableSize.height)).y;
break; break;
case Axis.horizontal: case Axis.horizontal:
Point targetCenter = targetBox.localToGlobal(new Point(targetSize.width / 2.0, 0.0)); targetMin = targetBox.localToGlobal(Point.origin).x;
Point scrollableCenter = scrollableBox.localToGlobal(new Point(scrollableSize.width / 2.0, 0.0)); targetMax = targetBox.localToGlobal(new Point(targetSize.width, 0.0)).x;
scrollOffsetDelta = targetCenter.x - scrollableCenter.x; scrollableMin = scrollableBox.localToGlobal(Point.origin).x;
scrollableMax = scrollableBox.localToGlobal(new Point(scrollableSize.width, 0.0)).x;
break; break;
} }
double scrollOffsetDelta;
if (targetMin < scrollableMin) {
if (targetMax > scrollableMax) {
// The target is to big to fit inside the scrollable. The best we can do
// is to center the target.
double targetCenter = (targetMin + targetMax) / 2.0;
double scrollableCenter = (scrollableMin + scrollableMax) / 2.0;
scrollOffsetDelta = targetCenter - scrollableCenter;
} else {
scrollOffsetDelta = targetMin - scrollableMin;
}
} else if (targetMax > scrollableMax) {
scrollOffsetDelta = targetMax - scrollableMax;
} else {
return new Future.value();
}
ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior; ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior;
double scrollOffset = (scrollable.scrollOffset + scrollOffsetDelta) double scrollOffset = (scrollable.scrollOffset + scrollOffsetDelta)
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset); .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
...@@ -281,7 +304,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -281,7 +304,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return _animateTo(newScrollOffset, duration, curve); return _animateTo(newScrollOffset, duration, curve);
} }
Future scrollBy(double scrollDelta, { Duration duration, Curve curve }) { Future scrollBy(double scrollDelta, { Duration duration, Curve curve: Curves.ease }) {
double newScrollOffset = scrollBehavior.applyCurve(_scrollOffset, scrollDelta); double newScrollOffset = scrollBehavior.applyCurve(_scrollOffset, scrollDelta);
return scrollTo(newScrollOffset, duration: duration, curve: curve); return scrollTo(newScrollOffset, duration: duration, curve: curve);
} }
......
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