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)