From 9e9d845b1fe7bd9f7512361d06b2389070b808ca Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Tue, 11 Aug 2015 15:42:21 -0700 Subject: [PATCH] Adds ensureWidgetIsVisible() function to scrollable.dart Set the scrollOffset of a widget's Scrollable ancestor so that the widget is centered within the scrollable. A future CL will add support for specifying exactly where the widget appears. The scroll can be animated by specifying the animation: parameter. Changed the duration Scrollable.scrollTo() parameter from a Duration to an AnimationPerformance so that one can configure all aspects of the animation. The caller may also listen to the animation to schedule other work while it updates or when its status changes. complete --- examples/widgets/ensure_visible.dart | 118 +++++++++++++++++++ packages/flutter/lib/widgets/scrollable.dart | 95 +++++++++++---- packages/flutter/lib/widgets/tabs.dart | 6 +- 3 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 examples/widgets/ensure_visible.dart diff --git a/examples/widgets/ensure_visible.dart b/examples/widgets/ensure_visible.dart new file mode 100644 index 00000000000..b0399a8c29f --- /dev/null +++ b/examples/widgets/ensure_visible.dart @@ -0,0 +1,118 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:sky/animation/animated_value.dart'; +import 'package:sky/animation/animation_performance.dart'; +import 'package:sky/animation/curves.dart'; +import 'package:sky/base/lerp.dart'; +import 'package:sky/painting/box_painter.dart'; +import 'package:sky/painting/text_style.dart'; +import 'package:sky/rendering/box.dart'; +import 'package:sky/theme/colors.dart'; +import 'package:sky/widgets/basic.dart'; +import 'package:sky/widgets/block_viewport.dart'; +import 'package:sky/widgets/card.dart'; +import 'package:sky/widgets/icon.dart'; +import 'package:sky/widgets/scrollable.dart'; +import 'package:sky/widgets/scaffold.dart'; +import 'package:sky/widgets/theme.dart'; +import 'package:sky/widgets/tool_bar.dart'; +import 'package:sky/widgets/framework.dart'; +import 'package:sky/widgets/task_description.dart'; + +class CardModel { + CardModel(this.value, this.height, this.color); + int value; + double height; + Color color; + String get label => "Card $value"; + Key get key => new Key.fromObjectIdentity(this); +} + +class EnsureVisibleApp extends App { + + static const TextStyle cardLabelStyle = + const TextStyle(color: white, fontSize: 18.0, fontWeight: bold); + + List cardModels; + BlockViewportLayoutState layoutState = new BlockViewportLayoutState(); + ScrollListener scrollListener; + AnimationPerformance scrollAnimation; + + void initState() { + List cardHeights = [ + 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0, + 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0, + 48.0, 63.0, 82.0, 146.0, 60.0, 55.0, 84.0, 96.0, 50.0 + ]; + cardModels = new List.generate(cardHeights.length, (i) { + Color color = lerpColor(Red[300], Blue[900], i / cardHeights.length); + return new CardModel(i, cardHeights[i], color); + }); + + scrollAnimation = new AnimationPerformance() + ..duration = const Duration(milliseconds: 200) + ..variable = new AnimatedValue(0.0, curve: ease); + + super.initState(); + } + + EventDisposition handleTap(Widget target) { + ensureWidgetIsVisible(target, animation: scrollAnimation); + return EventDisposition.processed; + } + + Widget builder(int index) { + if (index >= cardModels.length) + return null; + CardModel cardModel = cardModels[index]; + Widget card = new Card( + color: cardModel.color, + child: new Container( + height: cardModel.height, + padding: const EdgeDims.all(8.0), + child: new Center(child: new Text(cardModel.label, style: cardLabelStyle)) + ) + ); + return new Listener( + key: cardModel.key, + onGestureTap: (_) { return handleTap(card); }, + child: card + ); + } + + Widget build() { + Widget cardCollection = new Container( + padding: const EdgeDims.symmetric(vertical: 12.0, horizontal: 8.0), + decoration: new BoxDecoration(backgroundColor: Theme.of(this).primarySwatch[50]), + child: new VariableHeightScrollable( + builder: builder, + token: cardModels.length, + layoutState: layoutState + ) + ); + + return new IconTheme( + data: const IconThemeData(color: IconThemeColor.white), + child: new Theme( + data: new ThemeData( + brightness: ThemeBrightness.light, + primarySwatch: Blue, + accentColor: RedAccent[200] + ), + child: new TaskDescription( + label: 'Cards', + child: new Scaffold( + toolbar: new ToolBar(center: new Text('Tap a Card')), + body: cardCollection + ) + ) + ) + ); + } +} + +void main() { + runApp(new EnsureVisibleApp()); +} diff --git a/packages/flutter/lib/widgets/scrollable.dart b/packages/flutter/lib/widgets/scrollable.dart index cdc65f41763..5fb1a108774 100644 --- a/packages/flutter/lib/widgets/scrollable.dart +++ b/packages/flutter/lib/widgets/scrollable.dart @@ -9,8 +9,8 @@ import 'package:newton/newton.dart'; import 'package:sky/animation/animated_simulation.dart'; import 'package:sky/animation/animated_value.dart'; import 'package:sky/animation/animation_performance.dart'; -import 'package:sky/animation/curves.dart'; import 'package:sky/animation/scroll_behavior.dart'; +import 'package:sky/rendering/box.dart'; import 'package:sky/theme/view_configuration.dart' as config; import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/block_viewport.dart'; @@ -44,15 +44,10 @@ abstract class Scrollable extends StatefulComponent { ScrollDirection scrollDirection; AnimatedSimulation _toEndAnimation; // See _startToEndAnimation() - AnimationPerformance _toOffsetAnimation; // Started by scrollTo(offset, duration: d) + AnimationPerformance _toOffsetAnimation; // Started by scrollTo() void initState() { _toEndAnimation = new AnimatedSimulation(_tickScrollOffset); - _toOffsetAnimation = new AnimationPerformance() - ..addListener(() { - AnimatedValue offset = _toOffsetAnimation.variable; - scrollTo(offset.value); - }); } void syncFields(Scrollable source) { @@ -91,22 +86,39 @@ abstract class Scrollable extends StatefulComponent { ); } - void _startToOffsetAnimation(double newScrollOffset, Duration duration) { - _stopToEndAnimation(); + void _startToOffsetAnimation(double newScrollOffset, AnimationPerformance animation) { + _stopToEndAnimation(); + _stopToOffsetAnimation(); + + (animation.variable as AnimatedValue) + ..begin = scrollOffset + ..end = newScrollOffset; + + _toOffsetAnimation = animation + ..progress = 0.0 + ..addListener(_updateToOffsetAnimation) + ..addStatusListener(_updateToOffsetAnimationStatus) + ..play(); + } + + void _updateToOffsetAnimation() { + AnimatedValue offset = _toOffsetAnimation.variable; + scrollTo(offset.value); + } + + void _updateToOffsetAnimationStatus(AnimationStatus status) { + if (status == AnimationStatus.dismissed || status == AnimationStatus.completed) _stopToOffsetAnimation(); - _toOffsetAnimation - ..variable = new AnimatedValue(scrollOffset, - end: newScrollOffset, - curve: ease - ) - ..progress = 0.0 - ..duration = duration - ..play(); } void _stopToOffsetAnimation() { - if (_toOffsetAnimation.isAnimating) - _toOffsetAnimation.stop(); + if (_toOffsetAnimation != null) { + _toOffsetAnimation + ..removeStatusListener(_updateToOffsetAnimationStatus) + ..removeListener(_updateToOffsetAnimation) + ..stop(); + _toOffsetAnimation = null; + } } void _startToEndAnimation({ double velocity: 0.0 }) { @@ -127,16 +139,16 @@ abstract class Scrollable extends StatefulComponent { super.didUnmount(); } - bool scrollTo(double newScrollOffset, { Duration duration }) { + bool scrollTo(double newScrollOffset, { AnimationPerformance animation }) { if (newScrollOffset == _scrollOffset) return false; - if (duration == null) { + if (animation == null) { setState(() { _scrollOffset = newScrollOffset; }); } else { - _startToOffsetAnimation(newScrollOffset, duration); + _startToOffsetAnimation(newScrollOffset, animation); } if (_listeners.length > 0) @@ -178,7 +190,8 @@ abstract class Scrollable extends StatefulComponent { } void _maybeSettleScrollOffset() { - if (!_toEndAnimation.isAnimating && !_toOffsetAnimation.isAnimating) + if (!_toEndAnimation.isAnimating && + (_toOffsetAnimation == null || !_toOffsetAnimation.isAnimating)) settleScrollOffset(); } @@ -220,6 +233,42 @@ Scrollable findScrollableAncestor({ Widget target }) { return ancestor; } +bool ensureWidgetIsVisible(Widget target, { AnimationPerformance animation }) { + assert(target.mounted); + assert(target.root is RenderBox); + + Scrollable scrollable = findScrollableAncestor(target: target); + if (scrollable == null) + return false; + + Size targetSize = (target.root as RenderBox).size; + Point targetCenter = target.localToGlobal( + scrollable.scrollDirection == ScrollDirection.vertical + ? new Point(0.0, targetSize.height / 2.0) + : new Point(targetSize.width / 2.0, 0.0) + ); + + Size scrollableSize = (scrollable.root as RenderBox).size; + Point scrollableCenter = scrollable.localToGlobal( + scrollable.scrollDirection == ScrollDirection.vertical + ? new Point(0.0, scrollableSize.height / 2.0) + : new Point(scrollableSize.width / 2.0, 0.0) + ); + double scrollOffsetDelta = scrollable.scrollDirection == ScrollDirection.vertical + ? targetCenter.y - scrollableCenter.y + : targetCenter.x - scrollableCenter.x; + BoundedBehavior scrollBehavior = scrollable.scrollBehavior; + double scrollOffset = (scrollable.scrollOffset + scrollOffsetDelta) + .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset); + + if (scrollOffset != scrollable.scrollOffset) { + scrollable.scrollTo(scrollOffset, animation: animation); + return true; + } + + return false; +} + /// A simple scrollable widget that has a single child. Use this component if /// you are not worried about offscreen widgets consuming resources. class ScrollableViewport extends Scrollable { diff --git a/packages/flutter/lib/widgets/tabs.dart b/packages/flutter/lib/widgets/tabs.dart index ef374abc09b..090c5193833 100644 --- a/packages/flutter/lib/widgets/tabs.dart +++ b/packages/flutter/lib/widgets/tabs.dart @@ -407,12 +407,16 @@ class TabBar extends Scrollable { Size _tabBarSize; List _tabWidths; AnimationPerformance _indicatorAnimation; + AnimationPerformance _scrollAnimation; void initState() { super.initState(); _indicatorAnimation = new AnimationPerformance() ..duration = _kTabBarScroll ..variable = new AnimatedRect(null, curve: ease); + _scrollAnimation = new AnimationPerformance() + ..duration = _kTabBarScroll + ..variable = new AnimatedValue(0.0, curve: ease); } void syncFields(TabBar source) { @@ -468,7 +472,7 @@ class TabBar extends Scrollable { if (tabIndex != selectedIndex) { if (_tabWidths != null) { if (isScrollable) - scrollTo(_centeredTabScrollOffset(tabIndex), duration: _kTabBarScroll); + scrollTo(_centeredTabScrollOffset(tabIndex), animation: _scrollAnimation); _startIndicatorAnimation(selectedIndex, tabIndex); } if (onChanged != null)