mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Use SnackBar's SlideInIntention for Drawer animation.
This required some changes to AnimationPerformance to better understand animating with forces.
This commit is contained in:
parent
5f687552e5
commit
f7d97d4cd8
@ -69,8 +69,8 @@ class AnimatedList extends AnimatedVariable {
|
||||
String toString() => 'AnimatedList([$variables])';
|
||||
}
|
||||
|
||||
class AnimatedColor extends AnimatedValue<Color> {
|
||||
AnimatedColor(Color begin, { Color end, Curve curve: linear })
|
||||
class AnimatedColorValue extends AnimatedValue<Color> {
|
||||
AnimatedColorValue(Color begin, { Color end, Curve curve: linear })
|
||||
: super(begin, end: end, curve: curve);
|
||||
|
||||
void setProgress(double t) {
|
||||
|
||||
@ -8,10 +8,7 @@ import 'package:sky/animation/animated_value.dart';
|
||||
import 'package:sky/animation/forces.dart';
|
||||
import 'package:sky/animation/timeline.dart';
|
||||
|
||||
enum AnimationDirection {
|
||||
forward,
|
||||
reverse
|
||||
}
|
||||
export 'package:sky/animation/forces.dart' show Direction;
|
||||
|
||||
enum AnimationStatus {
|
||||
dismissed, // stoped at 0
|
||||
@ -39,8 +36,21 @@ class AnimationPerformance {
|
||||
Timeline _timeline;
|
||||
Timeline get timeline => _timeline;
|
||||
|
||||
AnimationDirection _direction;
|
||||
AnimationDirection get direction => _direction;
|
||||
Direction _direction;
|
||||
Direction get direction => _direction;
|
||||
|
||||
// If non-null, animate with this force instead of a tween animation.
|
||||
Force attachedForce;
|
||||
|
||||
void addVariable(AnimatedVariable newVariable) {
|
||||
if (variable == null) {
|
||||
variable = newVariable;
|
||||
} else if (variable is AnimatedList) {
|
||||
(variable as AnimatedList).variables.add(newVariable);
|
||||
} else {
|
||||
variable = new AnimatedList([variable, newVariable]);
|
||||
}
|
||||
}
|
||||
|
||||
double get progress => timeline.value;
|
||||
void set progress(double t) {
|
||||
@ -59,33 +69,34 @@ class AnimationPerformance {
|
||||
return AnimationStatus.completed;
|
||||
if (!isAnimating && progress == 0.0)
|
||||
return AnimationStatus.dismissed;
|
||||
return direction == AnimationDirection.forward ?
|
||||
return direction == Direction.forward ?
|
||||
AnimationStatus.forward :
|
||||
AnimationStatus.reverse;
|
||||
}
|
||||
|
||||
Future play([AnimationDirection direction = AnimationDirection.forward]) {
|
||||
Future play([Direction direction = Direction.forward]) {
|
||||
_direction = direction;
|
||||
return resume();
|
||||
}
|
||||
Future forward() => play(AnimationDirection.forward);
|
||||
Future reverse() => play(AnimationDirection.reverse);
|
||||
Future resume() => _animateTo(direction == AnimationDirection.forward ? 1.0 : 0.0);
|
||||
Future forward() => play(Direction.forward);
|
||||
Future reverse() => play(Direction.reverse);
|
||||
Future resume() {
|
||||
if (attachedForce != null)
|
||||
return fling(_direction, force: attachedForce);
|
||||
return _animateTo(direction == Direction.forward ? 1.0 : 0.0);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
timeline.stop();
|
||||
}
|
||||
|
||||
// Flings the timeline with an optional force (defaults to a critically damped
|
||||
// spring) and initial velocity. Negative velocity causes the timeline to go
|
||||
// in reverse.
|
||||
Future fling({double velocity: 1.0, Force force}) {
|
||||
// Flings the timeline in the given direction with an optional force
|
||||
// (defaults to a critically damped spring) and initial velocity.
|
||||
Future fling(Direction direction, {double velocity: 0.0, Force force}) {
|
||||
if (force == null)
|
||||
force = kDefaultSpringForce;
|
||||
// This is an approximation - the force may not necessarily result in
|
||||
// animating the same direction as the initial velocity.
|
||||
_direction = velocity >= 0.0 ? AnimationDirection.forward : AnimationDirection.reverse;
|
||||
return timeline.fling(force.release(progress, velocity));
|
||||
_direction = direction;
|
||||
return timeline.fling(force.release(progress, velocity, _direction));
|
||||
}
|
||||
|
||||
final List<Function> _listeners = new List<Function>();
|
||||
|
||||
@ -4,9 +4,15 @@
|
||||
|
||||
import 'package:newton/newton.dart';
|
||||
|
||||
// TODO(mpcomplete): This doesn't belong here.
|
||||
enum Direction {
|
||||
forward,
|
||||
reverse
|
||||
}
|
||||
|
||||
// Base class for creating Simulations for the animation Timeline.
|
||||
abstract class Force {
|
||||
Simulation release(double position, double velocity);
|
||||
Simulation release(double position, double velocity, Direction direction);
|
||||
}
|
||||
|
||||
class SpringForce extends Force {
|
||||
@ -17,18 +23,17 @@ class SpringForce extends Force {
|
||||
// respectively.
|
||||
final double left, right;
|
||||
|
||||
Simulation release(double position, double velocity) {
|
||||
Simulation release(double position, double velocity, Direction direction) {
|
||||
// Target just past the endpoint, because the animation will stop once the
|
||||
// Spring gets within the epsilon, and we want to stop at the endpoint.
|
||||
double target = velocity < 0.0 ?
|
||||
double target = direction == Direction.reverse ?
|
||||
this.left - _kEpsilon : this.right + _kEpsilon;
|
||||
return new SpringSimulation(spring, position, target, velocity);
|
||||
}
|
||||
}
|
||||
|
||||
final SpringDescription _kDefaultSpringDesc =
|
||||
new SpringDescription.withDampingRatio(
|
||||
mass: 1.0, springConstant: 500.0, ratio: 1.0);
|
||||
new SpringDescription.withDampingRatio(mass: 1.0, springConstant: 500.0, ratio: 1.0);
|
||||
final SpringForce kDefaultSpringForce = new SpringForce(_kDefaultSpringDesc);
|
||||
|
||||
const double _kEpsilon = 0.001;
|
||||
|
||||
@ -5,11 +5,10 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:newton/newton.dart';
|
||||
import 'package:sky/animation/forces.dart';
|
||||
|
||||
const double _kSecondsPerMillisecond = 1000.0;
|
||||
|
||||
abstract class ScrollBehavior extends Force {
|
||||
abstract class ScrollBehavior {
|
||||
Simulation release(double position, double velocity) => null;
|
||||
|
||||
// Returns the new scroll offset.
|
||||
|
||||
87
sky/packages/sky/lib/widgets/animation_intentions.dart
Normal file
87
sky/packages/sky/lib/widgets/animation_intentions.dart
Normal file
@ -0,0 +1,87 @@
|
||||
// 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.
|
||||
|
||||
// This file contains a set of common intentions to use with AnimationContainer
|
||||
// for describing how to animate certain properties.
|
||||
|
||||
import 'dart:sky';
|
||||
|
||||
import 'package:sky/animation/animated_value.dart';
|
||||
import 'package:sky/animation/animation_performance.dart';
|
||||
import 'package:sky/widgets/animated_container.dart';
|
||||
import 'package:sky/widgets/basic.dart';
|
||||
import 'package:vector_math/vector_math.dart';
|
||||
|
||||
// Slides a container in from |start| to |end| when the container's |tag| is
|
||||
// true (reverses if false).
|
||||
class SlideInIntention extends AnimationIntention {
|
||||
SlideInIntention({Duration duration, this.performance, Point start, Point end}) {
|
||||
if (performance == null) {
|
||||
assert(duration != null);
|
||||
performance = new AnimationPerformance(duration: duration);
|
||||
}
|
||||
_position = new AnimatedValue<Point>(start, end: end);
|
||||
performance.addVariable(_position);
|
||||
}
|
||||
|
||||
AnimatedValue<Point> _position;
|
||||
AnimationPerformance performance;
|
||||
|
||||
void initFields(AnimatedContainer container) {
|
||||
performance.addListener(() { _updateProgress(container); });
|
||||
performance.progress = 0.0;
|
||||
if (container.tag)
|
||||
performance.play();
|
||||
}
|
||||
|
||||
void syncFields(AnimatedContainer original, AnimatedContainer updated) {
|
||||
if (original.tag != updated.tag) {
|
||||
original.tag = updated.tag;
|
||||
performance.play(original.tag ? Direction.forward : Direction.reverse);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateProgress(AnimatedContainer container) {
|
||||
container.setState(() {
|
||||
container.transform = new Matrix4.identity()
|
||||
..translate(_position.value.x, _position.value.y);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Changes color from |start| to |end| when the container's |tag| is true
|
||||
// (reverses if false).
|
||||
class ColorTransitionIntention extends AnimationIntention {
|
||||
ColorTransitionIntention({Duration duration, this.performance, Color start, Color end}) {
|
||||
if (performance == null) {
|
||||
assert(duration != null);
|
||||
performance = new AnimationPerformance(duration: duration);
|
||||
}
|
||||
_color = new AnimatedColorValue(start, end: end);
|
||||
performance.addVariable(_color);
|
||||
}
|
||||
|
||||
AnimatedColorValue _color;
|
||||
AnimationPerformance performance;
|
||||
|
||||
void initFields(AnimatedContainer container) {
|
||||
performance.addListener(() { _updateProgress(container); });
|
||||
performance.progress = 0.0;
|
||||
if (container.tag)
|
||||
performance.play();
|
||||
}
|
||||
|
||||
void syncFields(AnimatedContainer original, AnimatedContainer updated) {
|
||||
if (original.tag != updated.tag) {
|
||||
original.tag = updated.tag;
|
||||
performance.play(original.tag ? Direction.forward : Direction.reverse);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateProgress(AnimatedContainer container) {
|
||||
container.setState(() {
|
||||
container.decoration = new BoxDecoration(backgroundColor: _color.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2,18 +2,19 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:sky' as sky;
|
||||
|
||||
import 'package:sky/animation/animated_value.dart';
|
||||
import 'package:sky/animation/animation_performance.dart';
|
||||
import 'package:sky/animation/forces.dart';
|
||||
import 'package:sky/theme/shadows.dart';
|
||||
import 'package:sky/theme/colors.dart' as colors;
|
||||
import 'package:sky/widgets/animated_component.dart';
|
||||
import 'package:sky/widgets/animated_container.dart';
|
||||
import 'package:sky/widgets/animation_intentions.dart';
|
||||
import 'package:sky/widgets/basic.dart';
|
||||
import 'package:sky/widgets/navigator.dart';
|
||||
import 'package:sky/widgets/scrollable_viewport.dart';
|
||||
import 'package:sky/widgets/theme.dart';
|
||||
import 'package:vector_math/vector_math.dart';
|
||||
|
||||
export 'package:sky/animation/animation_performance.dart' show AnimationStatus;
|
||||
|
||||
@ -34,12 +35,13 @@ const double _kWidth = 304.0;
|
||||
const double _kMinFlingVelocity = 365.0;
|
||||
const double _kFlingVelocityScale = 1.0 / 300.0;
|
||||
const Duration _kBaseSettleDuration = const Duration(milliseconds: 246);
|
||||
const Duration _kThemeChangeDuration = const Duration(milliseconds: 200);
|
||||
const Point _kOpenPosition = Point.origin;
|
||||
const Point _kClosedPosition = const Point(-_kWidth, 0.0);
|
||||
|
||||
typedef void DrawerStatusChangedCallback(AnimationStatus status);
|
||||
|
||||
class Drawer extends AnimatedComponent {
|
||||
class Drawer extends StatefulComponent {
|
||||
Drawer({
|
||||
Key key,
|
||||
this.children,
|
||||
@ -55,70 +57,58 @@ class Drawer extends AnimatedComponent {
|
||||
DrawerStatusChangedCallback onStatusChanged;
|
||||
Navigator navigator;
|
||||
|
||||
AnimatedValue<Point> _position;
|
||||
AnimatedColor _maskColor;
|
||||
AnimationPerformance _performance;
|
||||
SlideInIntention _intention;
|
||||
ColorTransitionIntention _maskColorIntention;
|
||||
AnimationPerformance get _performance => _intention.performance;
|
||||
|
||||
void initState() {
|
||||
_position = new AnimatedValue<Point>(_kClosedPosition, end: _kOpenPosition);
|
||||
_maskColor = new AnimatedColor(colors.transparent, end: const Color(0x7F000000));
|
||||
_performance = new AnimationPerformance()
|
||||
..duration = _kBaseSettleDuration
|
||||
..variable = new AnimatedList([_position, _maskColor])
|
||||
..addStatusListener(_onStatusChanged);
|
||||
watch(_performance);
|
||||
if (showing)
|
||||
_show();
|
||||
_intention = new SlideInIntention(
|
||||
duration: _kBaseSettleDuration, start: _kClosedPosition, end: _kOpenPosition);
|
||||
_maskColorIntention = new ColorTransitionIntention(
|
||||
performance: _intention.performance, start: colors.transparent, end: const Color(0x7F000000));
|
||||
|
||||
_performance.addStatusListener(_onStatusChanged);
|
||||
// Use a spring force for animating the drawer. We can't use curves for
|
||||
// this because we need a linear curve in order to track the user's finger
|
||||
// while dragging.
|
||||
_performance.attachedForce = kDefaultSpringForce;
|
||||
|
||||
if (navigator != null)
|
||||
navigator.pushState(this, (_) => _performance.reverse());
|
||||
}
|
||||
|
||||
void syncFields(Drawer source) {
|
||||
children = source.children;
|
||||
level = source.level;
|
||||
navigator = source.navigator;
|
||||
if (showing != source.showing) {
|
||||
showing = source.showing;
|
||||
showing ? _show() : _hide();
|
||||
}
|
||||
showing = source.showing;
|
||||
onStatusChanged = source.onStatusChanged;
|
||||
super.syncFields(source);
|
||||
}
|
||||
|
||||
void _show() {
|
||||
if (navigator != null)
|
||||
navigator.pushState(this, (_) => _performance.reverse());
|
||||
_fling(1.0);
|
||||
}
|
||||
|
||||
void _hide() {
|
||||
_fling(-1.0);
|
||||
}
|
||||
|
||||
// We fling the performance timeline instead of animating it to give it a
|
||||
// nice spring effect. We can't use curves for this because we need a linear
|
||||
// curve in order to track the user's finger while dragging.
|
||||
void _fling(double direction) {
|
||||
_performance.fling(velocity: direction.sign);
|
||||
}
|
||||
|
||||
Widget build() {
|
||||
var mask = new Listener(
|
||||
child: new Container(
|
||||
decoration: new BoxDecoration(backgroundColor: _maskColor.value)
|
||||
child: new AnimatedContainer(
|
||||
intentions: [_maskColorIntention],
|
||||
tag: showing
|
||||
),
|
||||
onGestureTap: handleMaskTap
|
||||
);
|
||||
|
||||
Matrix4 transform = new Matrix4.identity();
|
||||
transform.translate(_position.value.x, _position.value.y);
|
||||
Widget content = new Transform(
|
||||
transform: transform,
|
||||
child: new Container(
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: Theme.of(this).canvasColor,
|
||||
boxShadow: shadows[level]),
|
||||
width: _kWidth,
|
||||
child: new ScrollableBlock(children)
|
||||
));
|
||||
Widget content = new AnimatedContainer(
|
||||
intentions: [
|
||||
_intention,
|
||||
// TODO(mpcomplete): it should be easier to override some intentions,
|
||||
// and have those you don't care about revert to a sensible default.
|
||||
new ImplicitlySyncDecorationIntention(_kThemeChangeDuration),
|
||||
new ImplicitlySyncWidthIntention(_kThemeChangeDuration),
|
||||
],
|
||||
tag: showing,
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: Theme.of(this).canvasColor,
|
||||
boxShadow: shadows[level]),
|
||||
width: _kWidth,
|
||||
child: new ScrollableBlock(children)
|
||||
);
|
||||
|
||||
return new Listener(
|
||||
child: new Stack([ mask, content ]),
|
||||
@ -130,27 +120,27 @@ class Drawer extends AnimatedComponent {
|
||||
);
|
||||
}
|
||||
|
||||
double get xPosition => _position.value.x;
|
||||
|
||||
void _onStatusChanged(AnimationStatus status) {
|
||||
if (status == AnimationStatus.dismissed &&
|
||||
navigator != null &&
|
||||
navigator.currentRoute is RouteState &&
|
||||
(navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer
|
||||
navigator.pop();
|
||||
if (onStatusChanged != null)
|
||||
onStatusChanged(status);
|
||||
scheduleMicrotask(() {
|
||||
if (status == AnimationStatus.dismissed &&
|
||||
navigator != null &&
|
||||
navigator.currentRoute is RouteState &&
|
||||
(navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer
|
||||
navigator.pop();
|
||||
if (onStatusChanged != null)
|
||||
onStatusChanged(status);
|
||||
});
|
||||
}
|
||||
|
||||
bool get _isMostlyClosed => xPosition <= -_kWidth/2;
|
||||
bool get _isMostlyClosed => _performance.progress < 0.5;
|
||||
|
||||
void _settle() => _fling(_isMostlyClosed ? -1.0 : 1.0);
|
||||
void _settle() { _isMostlyClosed ? _performance.reverse() : _performance.play(); }
|
||||
|
||||
void handleMaskTap(_) => _fling(-1.0);
|
||||
void handleMaskTap(_) { _performance.reverse(); }
|
||||
|
||||
// TODO(mpcomplete): Figure out how to generalize these handlers on a
|
||||
// "PannableThingy" interface.
|
||||
void handlePointerDown(_) => _performance.stop();
|
||||
void handlePointerDown(_) { _performance.stop(); }
|
||||
|
||||
void handlePointerMove(sky.PointerEvent event) {
|
||||
if (_performance.isAnimating)
|
||||
@ -169,7 +159,10 @@ class Drawer extends AnimatedComponent {
|
||||
}
|
||||
|
||||
void handleFlingStart(event) {
|
||||
if (event.velocityX.abs() >= _kMinFlingVelocity)
|
||||
_performance.fling(velocity: event.velocityX * _kFlingVelocityScale);
|
||||
if (event.velocityX.abs() >= _kMinFlingVelocity) {
|
||||
_performance.fling(
|
||||
event.velocityX < 0.0 ? Direction.reverse : Direction.forward,
|
||||
velocity: event.velocityX.abs() * _kFlingVelocityScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,16 +4,15 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:sky/animation/animated_value.dart';
|
||||
import 'package:sky/animation/animation_performance.dart';
|
||||
import 'package:sky/painting/text_style.dart';
|
||||
import 'package:sky/theme/typography.dart' as typography;
|
||||
import 'package:sky/widgets/animated_container.dart';
|
||||
import 'package:sky/widgets/animation_intentions.dart';
|
||||
import 'package:sky/widgets/basic.dart';
|
||||
import 'package:sky/widgets/default_text_style.dart';
|
||||
import 'package:sky/widgets/material.dart';
|
||||
import 'package:sky/widgets/theme.dart';
|
||||
import 'package:vector_math/vector_math.dart';
|
||||
|
||||
export 'package:sky/animation/animation_performance.dart' show AnimationStatus;
|
||||
|
||||
@ -41,39 +40,6 @@ class SnackBarAction extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mpcomplete): generalize this to a SlideIn class.
|
||||
class SnackBarSlideInIntention extends AnimationIntention {
|
||||
SnackBarSlideInIntention(Duration duration) {
|
||||
_position = new AnimatedValue<Point>(const Point(0.0, 50.0), end: Point.origin);
|
||||
performance = new AnimationPerformance(duration: duration, variable: _position);
|
||||
}
|
||||
|
||||
SnackBarStatusChangedCallback onStatusChanged;
|
||||
AnimatedValue<Point> _position;
|
||||
AnimationPerformance performance;
|
||||
|
||||
void initFields(AnimatedContainer container) {
|
||||
performance.addListener(() { _updateProgress(container); });
|
||||
performance.progress = 0.0;
|
||||
if (container.tag)
|
||||
performance.play();
|
||||
}
|
||||
|
||||
void syncFields(AnimatedContainer original, AnimatedContainer updated) {
|
||||
if (original.tag != updated.tag) {
|
||||
original.tag = updated.tag;
|
||||
performance.play(original.tag ? AnimationDirection.forward : AnimationDirection.reverse);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateProgress(AnimatedContainer container) {
|
||||
container.setState(() {
|
||||
container.transform = new Matrix4.identity()
|
||||
..translate(_position.value.x, _position.value.y);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class SnackBar extends StatefulComponent {
|
||||
|
||||
SnackBar({
|
||||
@ -91,10 +57,12 @@ class SnackBar extends StatefulComponent {
|
||||
bool showing;
|
||||
SnackBarStatusChangedCallback onStatusChanged;
|
||||
|
||||
SnackBarSlideInIntention _intention;
|
||||
SlideInIntention _intention;
|
||||
|
||||
void initState() {
|
||||
_intention = new SnackBarSlideInIntention(_kSlideInDuration);
|
||||
_intention = new SlideInIntention(duration: _kSlideInDuration,
|
||||
start: const Point(0.0, 50.0),
|
||||
end: Point.origin);
|
||||
_intention.performance.addStatusListener(_onStatusChanged);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user