mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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_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)
|
||||
|
Loading…
Reference in New Issue
Block a user