mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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.
This commit is contained in:
parent
1e8c0816cc
commit
c0d7ed0be4
@ -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<double>(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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<RenderBox> { }
|
||||
@ -381,6 +382,9 @@ class TabBar extends Scrollable {
|
||||
SelectedIndexChanged onChanged;
|
||||
bool isScrollable;
|
||||
|
||||
Size _tabBarSize;
|
||||
List<double> _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<double> _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<double> tabWidths) {
|
||||
setState(() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user