前言
Flutter动画主分为两大类:补间动画、物理动画。
这里主要介绍几种方式实现动画效果。虽然使用的动画组件有所不同,但真正阅读源码分析会发现动画实现离不开几个关键对象:AnimationController、Animations、Tween、Ticker。
实现方式
AnimationController实现
AnimationController实现了Animation抽象类,T的Value为Double类型,其数值在0.0到1.0之间。可以认为是动画数值的控制器,通过内部Ticker实现数值变化。
class AnimationController extends Animation<double>
with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
AnimationController({
double value, //初始化数值
this.duration, //正向持续时间
this.reverseDuration,//逆向持续时间
this.debugLabel,
this.lowerBound = 0.0, //定义dismissed触发数值
this.upperBound = 1.0, //定义completed触发数值
this.animationBehavior = AnimationBehavior.normal,
@required TickerProvider vsync,
......
}
Demo代码
分别通过调用forward和reverse执行,设置addListener接口监听当前数值,addStatusListener接口监听动画执行状态,另外支持Stop、Reset等操作。AnimationStatus定义动画的四种状态: dismissed、forward、 reverse、 completed。forward和completed是一对状态,表示动画正向触发和正向触发完成并结束。reverse和dismissed是一对状态,表示动画逆向触发和逆向触发完成并结束。
PS:需要注意比如执行了forward并回调了completed动画状态,下次再次执行forward无法再次进行动画,因为当前AnimationController内部value已经是最大值1.0。可以在状态值返回completed后reset控制器或者执行forward时入参处理。
_controller = AnimationController(
animationBehavior: AnimationBehavior.preserve,
vsync: this, // the SingleTickerProviderStateMixin
duration: Duration(seconds: 2),
);
_controller.addListener(() {
//每次动画执行回调接口
_controller.value;
});
_controller.addStatusListener((status){
//动画状态变化回调接口
});
//正向执行动画
_controller.forward();
//逆向执行动画
_controller.reverse();
AnimatedContainer实现
AnimatedContainer可以算是傻瓜式动画实现,内部自带动画控制器。只要对它可执行动画的参数进行修改就能会有动画效果。它支持一些组件基础变化的动画过渡效果,例如长宽变、颜色变化、位置变化、边框变化等。
Demo代码
AnimatedContainer自身也是组件并继承自StatefulWidget,所以通过State去更新组件的一些参数。通过设置Duration控制动画持续时间,然后修改参数更新state后AnimatedContainer并会实现动画效果。当然也不用担心生命周期的问题,在组件移除时内部已经帮我们做好了dispose操作。
PS:暂时未找到可控制AnimatedContainer动画执行的方法。比如在执行过程中希望暂停或者取消等。可以认为AnimatedContainer傻瓜式的动画效果不可控制吧。
class AnimationContainerView extends StatefulWidget {
@override
_AnimationContainerViewState createState() => _AnimationContainerViewState();
}
class _AnimationContainerViewState extends State<AnimationContainerView> {
bool selected = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
selected = !selected;
});
},
child: Center(
child: AnimatedContainer(
width: selected ? 200.0 : 100.0,
height: selected ? 100.0 : 200.0,
color: selected ? Colors.red : Colors.blue,
alignment:
selected ? Alignment.center : AlignmentDirectional.topCenter,
duration: Duration(seconds: 2),
curve: Curves.fastOutSlowIn,
child: FlutterLogo(size: 75),
),
),
);
}
}
AnimatedWidget实现
AnimatedWidget就不像AnimatedContainer傻瓜式实现方式。实例化组件必填Listenable参数,Listenable可以是Animation或者ChangeNotifier。内部为Listenable设置监听,当外部动画控制器Control数值改变时通知到AnimatedWidget,根据数值大小变化从而实现组件动画效果。这样看来AnimatedWidget比AnimatedContainer灵活度高,需要实现的动画效果和执行由开发者控制。
abstract class AnimatedWidget extends StatefulWidget {
const AnimatedWidget({
Key key,
@required this.listenable,
}) : assert(listenable != null),
super(key: key);
......
}
class _AnimatedState extends State<AnimatedWidget> {
@override
void initState() {
super.initState();
widget.listenable.addListener(_handleChange);
}
@override
void didUpdateWidget(AnimatedWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.listenable != oldWidget.listenable) {
oldWidget.listenable.removeListener(_handleChange);
widget.listenable.addListener(_handleChange);
}
}
@override
void dispose() {
widget.listenable.removeListener(_handleChange);
super.dispose();
}
void _handleChange() {
setState(() {
});
}
@override
Widget build(BuildContext context) => widget.build(context);
}
Demo代码
动画的数值更新由外部动画控制器控制,所以可以通过控制AnimationController执行从而控制AnimatedWidget动画执行。例如实例AnimationController,通过Tween映射动画参数得到最终Animation。然后AnimationController执行动画,AnimatedWidget通过监听回调相应得到Animation最新数值去执行动画。
abstract class AnimatedWidgetView extends StatefulWidget {
@override
_AnimatedWidgetViewState createState() => _AnimatedWidgetViewState();
}
class _AnimatedWidgetViewState extends State<AnimatedWidgetView>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FlatButton(
child: Text("forward"),
onPressed: () {
controller.forward();
},
),
AnimatedLogo(
animation: animation,
),
FlatButton(
child: Text("reverse"),
onPressed: () {
controller.reverse();
},
),
],
);
}
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 10, end: 60).animate(controller);
}
}
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
Widget build(BuildContext context) {
final Animation<double> animation = listenable;
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
}
AnimatedBuilder实现
AnimatedBuilder继承AnimatedWidget。若使用AnimatedWidget实现动画必须继承它实现一个组件,而AnimatedBuilder并不关心实现动画组件只负责将动画与组件关联。在实例化组件多了TransitionBuilder成员实现内部布局。可以适用于创建复杂组件布局包含一部分动画并结合其他动画组件场景。
PS:个人觉得AnimatedBuilder像是装饰设计模式,对普通组件进行一次包装赋予新的动画能力。
class AnimatedBuilder extends AnimatedWidget {
const AnimatedBuilder({
Key key,
@required Listenable animation,
@required this.builder,
this.child,
}) : assert(animation != null),
assert(builder != null),
super(key: key, listenable: animation);
final TransitionBuilder builder;
final Widget child;
@override
Widget build(BuildContext context) {
return builder(context, child);
}
......
}
Demo代码
例如希望内部Container组件结合旋转操作实现动画。动画控制器和子组件是可动态配置,实现子组件与动画隔离。
....
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: Container(width: 200.0, height: 200.0, color: Colors.green),
builder: (BuildContext context, Widget child) {
return Transform.rotate(
angle: _controller.value * 2.0 * math.pi,
child: child,
);
},
);
}
.....
Hero实现
Hero动画和Android中元素共享效果相同,动画元素在切换页面时从一个页面过渡到另一个页面中。实现效果涉及到Hero、InkWell、Material以及Navigator,实例代码较多推荐参考官方文档。 官方例子
源码分析
以上几种动画实现方式归根结底还是离不开AnimationController的存在。例如AnimatedContainer内部state拥有AnimationController成员对象,AnimationController又是动画执行控制者。AnimationController是通过改变动画执行数值来控制动画执行,但当我直接使用double类型的数值赋值给Widget改变它的宽高并执行循环并通过setState去更新的时候却导致应用卡死崩溃。所以说执行动画离不开AnimationController。
AnimationController
AnimationController内部有TickerProvider成员,该成员用于创建Ticker。
例如执行控制器的正向执行方法代码:assert检查跳过,动画方向设置为forward,然后执行_animateToInternal私有方法。
TickerFuture forward({ double from }) {
_direction = _AnimationDirection.forward;
if (from != null)
value = from;
return _animateToInternal(upperBound);
}
_animateToInternal方法主要根据当前动画数值计算出_animateToInternal和是否继续执行动画,若target等于value也就是动画最终结束数值,则结束动画执行,反之继续执行_startSimulation方法。
TickerFuture _animateToInternal(double target, { Duration duration, Curve curve = Curves.linear }) {
double scale = 1.0;
if (SemanticsBinding.instance.disableAnimations) {
switch (animationBehavior) {
case AnimationBehavior.normal:
// Since the framework cannot handle zero duration animations, we run it at 5% of the normal
// duration to limit most animations to a single frame.
// TODO(jonahwilliams): determine a better process for setting duration.
scale = 0.05;
break;
case AnimationBehavior.preserve:
break;
}
}
Duration simulationDuration = duration;
if (simulationDuration == null) {
final double range = upperBound - lowerBound;
final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
final Duration directionDuration =
(_direction == _AnimationDirection.reverse && reverseDuration != null)
? reverseDuration
: this.duration;
simulationDuration = directionDuration * remainingFraction;
} else if (target == value) {
// Already at target, don't animate.
simulationDuration = Duration.zero;
}
// 先停止之前的动画
stop();
// 判断持续时间是否结束
if (simulationDuration == Duration.zero) {
if (value != target) {
_value = target.clamp(lowerBound, upperBound);
notifyListeners();
}
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed :
AnimationStatus.dismissed;
_checkStatusChanged();
//动画结束调用complete方法回调结果
return TickerFuture.complete();
}
// 动画还未结束继续调用_startSimulation方法。
return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));
}
执行_startSimulation方法,当前状态赋值最后执行_checkStatusChanged方法更新状态,这里的状态是AnimationStatus(dismissed、forward、 reverse、 completed)
TickerFuture _startSimulation(Simulation simulation) {
assert(simulation != null);
assert(!isAnimating);
_simulation = simulation;
_lastElapsedDuration = Duration.zero;
_value = simulation.x(0.0).clamp(lowerBound, upperBound);
final TickerFuture result = _ticker.start();
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.forward :
AnimationStatus.reverse;
_checkStatusChanged();
return result;
}
void _checkStatusChanged() {
final AnimationStatus newStatus = status;
if (_lastReportedStatus != newStatus) {
_lastReportedStatus = newStatus;
notifyStatusListeners(newStatus);
}
}
正向执行流程
forward -> _animateToInternal -> _startSimulation -> _checkStatusChanged
Ticker
Ticker用于监听动画帧,通过SchedulerBinding驱动执行。 在上述的_startSimulation方法可以知道有_ticker.start()执行。start方法中判断shouldScheduleTick是否去执行时间片更新函数,shouldScheduleTick 根据muted、isActive、scheduled进行判断,也就是通过shouldScheduleTick确认动画是否持续进行下去。
@protected
bool get shouldScheduleTick => !muted && isActive && !scheduled;
TickerFuture start() {
_future = TickerFuture._();
if (shouldScheduleTick) {
scheduleTick();
}
if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
_startTime = SchedulerBinding.instance.currentFrameTimeStamp;
return _future;
}
当执行scheduleTick方法调用SchedulerBinding的scheduleFrameCallback方法回调,根据情况在_tick中进行下一个scheduleTick从而实现动画连续绘制知道动画结束。
@protected
void scheduleTick({ bool rescheduling = false }) {
_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
void _tick(Duration timeStamp) {
assert(isTicking);
assert(scheduled);
_animationId = null;
_startTime ??= timeStamp;
_onTick(timeStamp - _startTime);
// The onTick callback may have scheduled another tick already, for
// example by calling stop then start again.
if (shouldScheduleTick)
scheduleTick(rescheduling: true);
}