diff --git a/packages/flutter/lib/src/animation/animation_controller.dart b/packages/flutter/lib/src/animation/animation_controller.dart index c2c7d62504..bccb784d78 100644 --- a/packages/flutter/lib/src/animation/animation_controller.dart +++ b/packages/flutter/lib/src/animation/animation_controller.dart @@ -145,13 +145,22 @@ class AnimationController extends Animation } 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) { diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 1e1d2cff1d..9292045733 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -366,7 +366,7 @@ class ScaffoldState extends State { } 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); diff --git a/packages/flutter/lib/src/widgets/focus.dart b/packages/flutter/lib/src/widgets/focus.dart index 15e12240ec..2a1e05cb41 100644 --- a/packages/flutter/lib/src/widgets/focus.dart +++ b/packages/flutter/lib/src/widgets/focus.dart @@ -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 { 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), diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index cecd3f4fa6..3774c37760 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -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 extends State { 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); }