Commit 9e9d845b authored by Hans Muller's avatar Hans Muller

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
parent c9c4852a
// 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<CardModel> cardModels;
BlockViewportLayoutState layoutState = new BlockViewportLayoutState();
ScrollListener scrollListener;
AnimationPerformance scrollAnimation;
void initState() {
List<double> cardHeights = <double>[
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<double>(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());
}
......@@ -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<double> 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<double>)
..begin = scrollOffset
..end = newScrollOffset;
_toOffsetAnimation = animation
..progress = 0.0
..addListener(_updateToOffsetAnimation)
..addStatusListener(_updateToOffsetAnimationStatus)
..play();
}
void _updateToOffsetAnimation() {
AnimatedValue<double> offset = _toOffsetAnimation.variable;
scrollTo(offset.value);
}
void _updateToOffsetAnimationStatus(AnimationStatus status) {
if (status == AnimationStatus.dismissed || status == AnimationStatus.completed)
_stopToOffsetAnimation();
_toOffsetAnimation
..variable = new AnimatedValue<double>(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 {
......
......@@ -407,12 +407,16 @@ class TabBar extends Scrollable {
Size _tabBarSize;
List<double> _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<double>(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)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment