Merge pull request #572 from HansMuller/ensure-visible
Adds ensureWidgetIsVisible() function to scrollable.dart
This commit is contained in:
commit
16ffdc67a6
118
examples/widgets/ensure_visible.dart
Normal file
118
examples/widgets/ensure_visible.dart
Normal file
@ -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<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_simulation.dart';
|
||||||
import 'package:sky/animation/animated_value.dart';
|
import 'package:sky/animation/animated_value.dart';
|
||||||
import 'package:sky/animation/animation_performance.dart';
|
import 'package:sky/animation/animation_performance.dart';
|
||||||
import 'package:sky/animation/curves.dart';
|
|
||||||
import 'package:sky/animation/scroll_behavior.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/theme/view_configuration.dart' as config;
|
||||||
import 'package:sky/widgets/basic.dart';
|
import 'package:sky/widgets/basic.dart';
|
||||||
import 'package:sky/widgets/block_viewport.dart';
|
import 'package:sky/widgets/block_viewport.dart';
|
||||||
@ -44,15 +44,10 @@ abstract class Scrollable extends StatefulComponent {
|
|||||||
ScrollDirection scrollDirection;
|
ScrollDirection scrollDirection;
|
||||||
|
|
||||||
AnimatedSimulation _toEndAnimation; // See _startToEndAnimation()
|
AnimatedSimulation _toEndAnimation; // See _startToEndAnimation()
|
||||||
AnimationPerformance _toOffsetAnimation; // Started by scrollTo(offset, duration: d)
|
AnimationPerformance _toOffsetAnimation; // Started by scrollTo()
|
||||||
|
|
||||||
void initState() {
|
void initState() {
|
||||||
_toEndAnimation = new AnimatedSimulation(_tickScrollOffset);
|
_toEndAnimation = new AnimatedSimulation(_tickScrollOffset);
|
||||||
_toOffsetAnimation = new AnimationPerformance()
|
|
||||||
..addListener(() {
|
|
||||||
AnimatedValue<double> offset = _toOffsetAnimation.variable;
|
|
||||||
scrollTo(offset.value);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void syncFields(Scrollable source) {
|
void syncFields(Scrollable source) {
|
||||||
@ -91,22 +86,39 @@ abstract class Scrollable extends StatefulComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startToOffsetAnimation(double newScrollOffset, Duration duration) {
|
void _startToOffsetAnimation(double newScrollOffset, AnimationPerformance animation) {
|
||||||
_stopToEndAnimation();
|
_stopToEndAnimation();
|
||||||
_stopToOffsetAnimation();
|
_stopToOffsetAnimation();
|
||||||
_toOffsetAnimation
|
|
||||||
..variable = new AnimatedValue<double>(scrollOffset,
|
(animation.variable as AnimatedValue<double>)
|
||||||
end: newScrollOffset,
|
..begin = scrollOffset
|
||||||
curve: ease
|
..end = newScrollOffset;
|
||||||
)
|
|
||||||
|
_toOffsetAnimation = animation
|
||||||
..progress = 0.0
|
..progress = 0.0
|
||||||
..duration = duration
|
..addListener(_updateToOffsetAnimation)
|
||||||
|
..addStatusListener(_updateToOffsetAnimationStatus)
|
||||||
..play();
|
..play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateToOffsetAnimation() {
|
||||||
|
AnimatedValue<double> offset = _toOffsetAnimation.variable;
|
||||||
|
scrollTo(offset.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateToOffsetAnimationStatus(AnimationStatus status) {
|
||||||
|
if (status == AnimationStatus.dismissed || status == AnimationStatus.completed)
|
||||||
|
_stopToOffsetAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
void _stopToOffsetAnimation() {
|
void _stopToOffsetAnimation() {
|
||||||
if (_toOffsetAnimation.isAnimating)
|
if (_toOffsetAnimation != null) {
|
||||||
_toOffsetAnimation.stop();
|
_toOffsetAnimation
|
||||||
|
..removeStatusListener(_updateToOffsetAnimationStatus)
|
||||||
|
..removeListener(_updateToOffsetAnimation)
|
||||||
|
..stop();
|
||||||
|
_toOffsetAnimation = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startToEndAnimation({ double velocity: 0.0 }) {
|
void _startToEndAnimation({ double velocity: 0.0 }) {
|
||||||
@ -127,16 +139,16 @@ abstract class Scrollable extends StatefulComponent {
|
|||||||
super.didUnmount();
|
super.didUnmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool scrollTo(double newScrollOffset, { Duration duration }) {
|
bool scrollTo(double newScrollOffset, { AnimationPerformance animation }) {
|
||||||
if (newScrollOffset == _scrollOffset)
|
if (newScrollOffset == _scrollOffset)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (duration == null) {
|
if (animation == null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_scrollOffset = newScrollOffset;
|
_scrollOffset = newScrollOffset;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_startToOffsetAnimation(newScrollOffset, duration);
|
_startToOffsetAnimation(newScrollOffset, animation);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_listeners.length > 0)
|
if (_listeners.length > 0)
|
||||||
@ -178,7 +190,8 @@ abstract class Scrollable extends StatefulComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _maybeSettleScrollOffset() {
|
void _maybeSettleScrollOffset() {
|
||||||
if (!_toEndAnimation.isAnimating && !_toOffsetAnimation.isAnimating)
|
if (!_toEndAnimation.isAnimating &&
|
||||||
|
(_toOffsetAnimation == null || !_toOffsetAnimation.isAnimating))
|
||||||
settleScrollOffset();
|
settleScrollOffset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,6 +233,42 @@ Scrollable findScrollableAncestor({ Widget target }) {
|
|||||||
return ancestor;
|
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
|
/// A simple scrollable widget that has a single child. Use this component if
|
||||||
/// you are not worried about offscreen widgets consuming resources.
|
/// you are not worried about offscreen widgets consuming resources.
|
||||||
class ScrollableViewport extends Scrollable {
|
class ScrollableViewport extends Scrollable {
|
||||||
|
@ -407,12 +407,16 @@ class TabBar extends Scrollable {
|
|||||||
Size _tabBarSize;
|
Size _tabBarSize;
|
||||||
List<double> _tabWidths;
|
List<double> _tabWidths;
|
||||||
AnimationPerformance _indicatorAnimation;
|
AnimationPerformance _indicatorAnimation;
|
||||||
|
AnimationPerformance _scrollAnimation;
|
||||||
|
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_indicatorAnimation = new AnimationPerformance()
|
_indicatorAnimation = new AnimationPerformance()
|
||||||
..duration = _kTabBarScroll
|
..duration = _kTabBarScroll
|
||||||
..variable = new AnimatedRect(null, curve: ease);
|
..variable = new AnimatedRect(null, curve: ease);
|
||||||
|
_scrollAnimation = new AnimationPerformance()
|
||||||
|
..duration = _kTabBarScroll
|
||||||
|
..variable = new AnimatedValue<double>(0.0, curve: ease);
|
||||||
}
|
}
|
||||||
|
|
||||||
void syncFields(TabBar source) {
|
void syncFields(TabBar source) {
|
||||||
@ -468,7 +472,7 @@ class TabBar extends Scrollable {
|
|||||||
if (tabIndex != selectedIndex) {
|
if (tabIndex != selectedIndex) {
|
||||||
if (_tabWidths != null) {
|
if (_tabWidths != null) {
|
||||||
if (isScrollable)
|
if (isScrollable)
|
||||||
scrollTo(_centeredTabScrollOffset(tabIndex), duration: _kTabBarScroll);
|
scrollTo(_centeredTabScrollOffset(tabIndex), animation: _scrollAnimation);
|
||||||
_startIndicatorAnimation(selectedIndex, tabIndex);
|
_startIndicatorAnimation(selectedIndex, tabIndex);
|
||||||
}
|
}
|
||||||
if (onChanged != null)
|
if (onChanged != null)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user