From c0d7ed0be43c406fd1c2e9d248fb253145b9f505 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Mon, 27 Jul 2015 14:02:02 -0700 Subject: [PATCH] When the selected tab changes in a TabBar for which isScrollable:true, animate the selected tab to the center of the viewport, if that's possible. Added an optional duration: to Scrollable.scrollTo(newOffset, duration: d) which animates the scroll to newOffset. Refactored the Scrollable implementation a little to make the roles of the "toEnd" and "toOffset" animations clearer. _handlePointerDown() now stops both animations. Similarly, we only call settleScrollOffset() from the cancel gesture handlers if the animations aren't already running. --- sky/packages/sky/lib/widgets/scrollable.dart | 103 +++++++++++++------ sky/packages/sky/lib/widgets/tabs.dart | 25 ++++- 2 files changed, 90 insertions(+), 38 deletions(-) diff --git a/sky/packages/sky/lib/widgets/scrollable.dart b/sky/packages/sky/lib/widgets/scrollable.dart index f5e566507ec..f6bafde0483 100644 --- a/sky/packages/sky/lib/widgets/scrollable.dart +++ b/sky/packages/sky/lib/widgets/scrollable.dart @@ -5,7 +5,10 @@ import 'dart:sky' as sky; import 'package:newton/newton.dart'; +import 'package:sky/animation/animation_performance.dart'; import 'package:sky/animation/animated_simulation.dart'; +import 'package:sky/animation/animated_value.dart'; +import 'package:sky/animation/curves.dart'; import 'package:sky/animation/scroll_behavior.dart'; import 'package:sky/theme/view_configuration.dart' as config; import 'package:sky/widgets/basic.dart'; @@ -33,8 +36,15 @@ abstract class Scrollable extends StatefulComponent { ScrollDirection direction; + AnimatedSimulation _toEndAnimation; // See _startToEndAnimation() + AnimationPerformance _toOffsetAnimation; // Started by scrollTo(offset, duration: d) + void initState() { - _animation = new AnimatedSimulation(_tickScrollOffset); + _toEndAnimation = new AnimatedSimulation(_tickScrollOffset); + _toOffsetAnimation = new AnimationPerformance() + ..addListener(() { + scrollTo(_toOffsetAnimation.variable.value); + }); } void syncFields(Scrollable source) { @@ -52,8 +62,6 @@ abstract class Scrollable extends StatefulComponent { return _scrollBehavior; } - AnimatedSimulation _animation; - Widget buildContent(); Widget build() { @@ -87,12 +95,54 @@ abstract class Scrollable extends StatefulComponent { }); } - bool scrollTo(double newScrollOffset) { + void _startToOffsetAnimation(double newScrollOffset, Duration duration) { + _stopToEndAnimation(); + _stopToOffsetAnimation(); + _toOffsetAnimation + ..variable = new AnimatedValue(scrollOffset, + end: newScrollOffset, + curve: ease + ) + ..progress = 0.0 + ..duration = duration + ..play(); + } + + void _stopToOffsetAnimation() { + if (_toOffsetAnimation.isAnimating) + _toOffsetAnimation.stop(); + } + + void _startToEndAnimation({ double velocity: 0.0 }) { + _stopToEndAnimation(); + _stopToOffsetAnimation(); + Simulation simulation = scrollBehavior.release(scrollOffset, velocity); + if (simulation != null) + _toEndAnimation.start(simulation); + } + + void _stopToEndAnimation() { + _toEndAnimation.stop(); + } + + void didUnmount() { + _stopToEndAnimation(); + _stopToOffsetAnimation(); + super.didUnmount(); + } + + bool scrollTo(double newScrollOffset, { Duration duration }) { if (newScrollOffset == _scrollOffset) return false; - setState(() { - _scrollOffset = newScrollOffset; - }); + + if (duration == null) { + setState(() { + _scrollOffset = newScrollOffset; + }); + } else { + _startToOffsetAnimation(newScrollOffset, duration); + } + if (_registeredScrollClients != null) { var newList = null; _registeredScrollClients.forEach((target) { @@ -114,24 +164,8 @@ abstract class Scrollable extends StatefulComponent { return scrollTo(newScrollOffset); } - void didUnmount() { - _stopSimulation(); - super.didUnmount(); - } - void settleScrollOffset() { - _startSimulation(); - } - - void _stopSimulation() { - _animation.stop(); - } - - void _startSimulation({ double velocity: 0.0 }) { - _stopSimulation(); - Simulation simulation = scrollBehavior.release(scrollOffset, velocity); - if (simulation != null) - _animation.start(simulation); + _startToEndAnimation(); } void _tickScrollOffset(double value) { @@ -139,12 +173,8 @@ abstract class Scrollable extends StatefulComponent { } void _handlePointerDown(_) { - _stopSimulation(); - } - - void _handlePointerUpOrCancel(_) { - if (!_animation.isAnimating) - settleScrollOffset(); + _stopToEndAnimation(); + _stopToOffsetAnimation(); } void _handleScrollUpdate(sky.GestureEvent event) { @@ -155,15 +185,20 @@ abstract class Scrollable extends StatefulComponent { double eventVelocity = direction == ScrollDirection.horizontal ? -event.velocityX : -event.velocityY; - _startSimulation(velocity: _velocityForFlingGesture(eventVelocity)); + _startToEndAnimation(velocity: _velocityForFlingGesture(eventVelocity)); } - void _handleFlingCancel(sky.GestureEvent event) { - settleScrollOffset(); + void _maybeSettleScrollOffset() { + if (!_toEndAnimation.isAnimating && !_toOffsetAnimation.isAnimating) + settleScrollOffset(); } + void _handlePointerUpOrCancel(_) { _maybeSettleScrollOffset(); } + + void _handleFlingCancel(sky.GestureEvent event) { _maybeSettleScrollOffset(); } + + void _handleWheel(sky.WheelEvent event) { scrollBy(-event.offsetY); } - } diff --git a/sky/packages/sky/lib/widgets/tabs.dart b/sky/packages/sky/lib/widgets/tabs.dart index 0676a8de721..156e6d0d473 100644 --- a/sky/packages/sky/lib/widgets/tabs.dart +++ b/sky/packages/sky/lib/widgets/tabs.dart @@ -34,6 +34,7 @@ const EdgeDims _kTabLabelPadding = const EdgeDims.symmetric(horizontal: 12.0); const TextStyle _kTabTextStyle = const TextStyle(textAlign: TextAlign.center); const int _kTabIconSize = 24; const double _kTabBarScrollDrag = 0.025; +const Duration _kTabBarScroll = const Duration(milliseconds: 200); class TabBarParentData extends BoxParentData with ContainerParentDataMixin { } @@ -381,6 +382,9 @@ class TabBar extends Scrollable { SelectedIndexChanged onChanged; bool isScrollable; + Size _tabBarSize; + List _tabWidths; + void syncFields(TabBar source) { super.syncFields(source); labels = source.labels; @@ -396,8 +400,13 @@ class TabBar extends Scrollable { _TabsScrollBehavior get scrollBehavior => super.scrollBehavior; void _handleTap(int tabIndex) { - if (tabIndex != selectedIndex && onChanged != null) - onChanged(tabIndex); + if (tabIndex != selectedIndex) { + if (_tabWidths != null) { + scrollTo(_centeredTabScrollOffset(tabIndex), duration: _kTabBarScroll); + } + if (onChanged != null) + onChanged(tabIndex); + } } Widget _toTab(TabLabel label, int tabIndex) { @@ -410,8 +419,16 @@ class TabBar extends Scrollable { ); } - Size _tabBarSize; - List _tabWidths; + double _centeredTabScrollOffset(int tabIndex) { + assert(_tabWidths != null); + assert(tabIndex >= 0 && tabIndex < _tabWidths.length); + double viewportWidth = scrollBehavior.containerSize; + double tabOffset = 0.0; + if (tabIndex > 0) + tabOffset = _tabWidths.take(tabIndex).reduce((sum, width) => sum + width); + return (tabOffset + _tabWidths[tabIndex] / 2.0 - viewportWidth / 2.0) + .clamp(scrollBehavior.minScrollOffset, scrollBehavior.maxScrollOffset); + } void _layoutChanged(Size tabBarSize, List tabWidths) { setState(() {