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.
This commit is contained in:
Adam Barth
2016-01-21 16:13:28 -08:00
parent 72931955c8
commit dd5df79e7b
4 changed files with 79 additions and 16 deletions

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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),

View File

@@ -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);
}