Commit dd5df79e authored by Adam Barth's avatar Adam Barth

Scroll focused input widgets into view

When opening the keyboard or focusing an input widget, we should scroll the
widget into view so that the user can see what they're typing.
parent 72931955
......@@ -145,13 +145,22 @@ class AnimationController extends Animation<double>
}
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();
if (remainingDuration == Duration.ZERO)
if (simulationDuration == Duration.ZERO) {
assert(value == target);
return new Future.value();
assert(remainingDuration > Duration.ZERO);
}
assert(simulationDuration > Duration.ZERO);
assert(!isAnimating);
return _startSimulation(new _TweenSimulation(_value, target, remainingDuration, curve));
return _startSimulation(new _TweenSimulation(_value, target, simulationDuration, curve));
}
Future _startSimulation(Simulation simulation) {
......
......@@ -366,7 +366,7 @@ class ScaffoldState extends State<Scaffold> {
}
Widget build(BuildContext context) {
EdgeDims padding = MediaQuery.of(context).padding;
EdgeDims padding = MediaQuery.of(context)?.padding ?? EdgeDims.zero;
if (_snackBars.length > 0) {
ModalRoute route = ModalRoute.of(context);
......
......@@ -2,7 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'basic.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
// component's subscopes (e.g. dialogs) are focused. This is distinct from the
......@@ -130,10 +135,13 @@ class Focus extends StatefulComponent {
/// 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.
static void moveTo(GlobalKey key) {
assert(key.currentContext != null);
BuildContext focusedContext = key.currentContext;
assert(focusedContext != null);
_FocusScope focusScope = key.currentContext.ancestorWidgetOfExactType(_FocusScope);
if (focusScope != null)
if (focusScope != null) {
focusScope.focusState._setFocusedWidget(key);
Scrollable.ensureVisible(focusedContext);
}
}
/// Focuses a particular focus scope, identified by its GlobalKey. The widget
......@@ -239,7 +247,30 @@ class FocusState extends State<Focus> {
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) {
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(
focusState: this,
scopeFocused: Focus._atScope(context),
......
......@@ -64,7 +64,7 @@ abstract class Scrollable extends StatefulComponent {
}
/// 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);
// TODO(abarth): This function doesn't handle nested scrollable widgets.
......@@ -80,20 +80,43 @@ abstract class Scrollable extends StatefulComponent {
assert(scrollableBox.attached);
Size scrollableSize = scrollableBox.size;
double scrollOffsetDelta;
double targetMin;
double targetMax;
double scrollableMin;
double scrollableMax;
switch (scrollable.config.scrollDirection) {
case Axis.vertical:
Point targetCenter = targetBox.localToGlobal(new Point(0.0, targetSize.height / 2.0));
Point scrollableCenter = scrollableBox.localToGlobal(new Point(0.0, scrollableSize.height / 2.0));
scrollOffsetDelta = targetCenter.y - scrollableCenter.y;
targetMin = targetBox.localToGlobal(Point.origin).y;
targetMax = targetBox.localToGlobal(new Point(0.0, targetSize.height)).y;
scrollableMin = scrollableBox.localToGlobal(Point.origin).y;
scrollableMax = scrollableBox.localToGlobal(new Point(0.0, scrollableSize.height)).y;
break;
case Axis.horizontal:
Point targetCenter = targetBox.localToGlobal(new Point(targetSize.width / 2.0, 0.0));
Point scrollableCenter = scrollableBox.localToGlobal(new Point(scrollableSize.width / 2.0, 0.0));
scrollOffsetDelta = targetCenter.x - scrollableCenter.x;
targetMin = targetBox.localToGlobal(Point.origin).x;
targetMax = targetBox.localToGlobal(new Point(targetSize.width, 0.0)).x;
scrollableMin = scrollableBox.localToGlobal(Point.origin).x;
scrollableMax = scrollableBox.localToGlobal(new Point(scrollableSize.width, 0.0)).x;
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;
double scrollOffset = (scrollable.scrollOffset + scrollOffsetDelta)
.clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset);
......@@ -281,7 +304,7 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
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);
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