构建一个基本的动画组件

基础结构设计

1// 1. 首先声明组件结构
2#[derive(Live, Widget)]
3pub struct TypingAnimation {
4    #[deref] view: View,          // 基础视图组件
5    #[animator] animator: Animator,  // 动画控制器
6
7    // 动画参数
8    #[live(0.65)] animation_duration: f64,
9    #[live(10.0)] swing_amplitude: f64,
10    #[live(3.0)] swing_base: f64,
11
12    // 运行时状态
13    #[rust] next_frame: Option<NextFrame>,
14    #[rust] animation_start_time: f64,
15    #[rust] is_animating: bool,
16}

这个结构为我们的动画组件提供了必要的基础设施。

动画状态定义

在 Makepad 中,我们使用 live_design! 宏来声明动画状态:

1live_design! {
2    TypingAnimation = {{TypingAnimation}} {
3        // 基础属性配置
4        width: Fit,
5        height: Fit,
6
7        // 动画状态定义
8        animator: {
9	        // 动画状态组
10            circle1 = {
11                default: down,  // 默认状态
12                down = {       // 向下移动状态
13                    redraw: true,
14                    from: {all: Forward {duration: 0.325}}
15                    ease: InOutQuad
16                    apply: {content = { circle1 = { margin: {top: 10.0} }}}
17                }
18                up = {        // 向上移动状态
19                    redraw: true,
20                    from: {all: Forward {duration: 0.325}}
21                    ease: InOutQuad
22                    apply: {content = { circle1 = { margin: {top: 3.0} }}}
23                }
24            }
25        }
26    }
27}

在 Makepad 的 Live DSL 中,animator 是一个特殊的属性,用于定义组件的动画状态和行为。让我们深入了解它的语法和结构。

  • 动画状态组是一个命名空间,用于组织相关的动画状态。一个状态组就是一个动画轨道(track)。所以当你想要对同一个对象的不同属性进行动画时,需要为每个动画属性创建独立的 track。
1animator: {
2    circle1_position = {  // 位置的 track
3        default: down,
4        down = { /* 控制位置的动画状态 */ }
5    }
6    circle1_opacity = {   // 透明度的 track
7        default: visible,
8        hidden = { /* 控制透明度的动画状态 */ }
9    }
10}
  • 每个状态包含三个主要部分。
    • from - 定义动画的时间特性
    • ease - 定义动画的缓动函数
    • apply - 定义状态的最终属性值
1// from
2from: {
3    all: Forward {duration: 0.2}      // 正向播放一次
4    all: Reverse {                    // 反向播放一次
5        duration: 0.2,
6        end: 1.0
7    }
8    all: Loop {                       // 循环播放
9        duration: 0.2,
10        end: 1.0
11    }
12    all: BounceLoop {                 // 来回循环播放
13        duration: 0.2,
14        end: 1.0
15    }
16    all: Snap                         // 瞬间切换,无动画
17}
18// ease
19// - `OutQuad`/`OutCubic`: 适用于自然运动
20// - `InOutQuad`: 适用于可逆动画
21// - `Linear`: 适用于旋转等持续动画
22ease: Linear          // 线性
23ease: InQuad          // 二次方加速
24ease: OutQuad         // 二次方减速
25ease: InOutQuad       // 二次方加速减速
26ease: Bezier {        // 贝塞尔曲线
27    cp0: 0.0,
28    cp1: 0.0,
29    cp2: 1.0,
30    cp3: 1.0
31}
32
33// apply
34apply: {
35    opacity: 1.0,
36    scale: 1.2,
37    color: #f00,
38    position: vec2(100.0, 0.0)
39}

实现动画逻辑

动画的核心在于状态管理和帧更新:

1impl Widget for TypingAnimation {
2    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
3        match event {
4            Event::NextFrame(ne) => {
5                // 处理动画帧更新
6                if let Some(next_frame) = self.next_frame {
7                    if ne.set.contains(&next_frame) {
8                        self.update_animation(cx, ne.time);
9                        self.next_frame = Some(cx.new_next_frame());
10                    }
11                }
12            }
13            // ... 其他事件处理
14        }
15    }
16}

注意: Makepad 中实现动画要基于 NextFrame ,而非 Timer。因为 Timer 依赖于底层 OS API ,有跨平台的风险。

在代码中触发动画的方法:

1// 瞬间切换状态
2self.animator_cut(cx, &[id!(hover), id!(on)]);
3
4// 播放动画过渡到状态
5self.animator_play(cx, &[id!(hover), id!(on)]);
6
7// 根据条件切换状态
8self.animator_toggle(
9    cx,
10    is_hovered,                    // 条件
11    Animate::Yes,                  // 是否需要动画
12    &[id!(hover), id!(on)],       // 条件为真时的状态
13    &[id!(hover), id!(off)]       // 条件为假时的状态
14);

处理动画事件:

1fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
2    // 处理动画系统事件
3    if self.animator_handle_event(cx, event).must_redraw() {
4        // 动画需要重绘
5        self.redraw(cx);
6    }
7
8    // 检查动画状态
9    if self.animator_in_state(cx, &[id!(hover), id!(on)]) {
10        // 当前处于悬停状态
11    }
12}

动画控制接口

为动画组件提供控制接口是良好实践:

1impl TypingAnimationRef {
2    // 启动动画
3    pub fn start(&self, cx: &mut Cx) {
4        if let Some(mut inner) = self.borrow_mut() {
5            inner.start(cx);
6        }
7    }
8
9    // 停止动画
10    pub fn stop(&self, cx: &mut Cx) {
11        if let Some(mut inner) = self.borrow_mut() {
12            inner.stop(cx);
13        }
14    }
15
16    // 配置动画参数
17    pub fn set_animation_speed(&self, cx: &mut Cx, duration: f64) {
18        if let Some(mut inner) = self.borrow_mut() {
19            inner.animation_duration = duration;
20        }
21    }
22}