完整动画实例

1use makepad_widgets::*;
2
3live_design! {
4    import makepad_widgets::base::*;
5    import makepad_widgets::theme_desktop_dark::*;
6    import crate::shared::styles::*;
7
8    ANIMATION_DURATION = 0.65
9    DEFAULT_SWING_TOP = 10.0
10    DEFAULT_SWING_BOTTOM = 3.0
11
12    // 1. Set the width and height to the same value.
13    // 2. Set the radius to half of the width/height.
14    EllipsisDot = <CircleView> {
15        width: 3
16        height: 3
17        draw_bg: {
18            radius: 1.5
19            color: (TYPING_NOTICE_TEXT_COLOR)
20        }
21    }
22
23    TypingAnimation = {{TypingAnimation}} {
24        width: Fit,
25        height: Fit,
26
27        // 添加可配置的动画参数
28        swing_amplitude: (DEFAULT_SWING_TOP), // 小球摆动的最大高度
29        swing_base: (DEFAULT_SWING_BOTTOM),   // 小球摆动的基础高度
30        animation_duration: (ANIMATION_DURATION), // 动画周期
31
32        flow: Down,
33        align: {x: 0.0, y: 0.5},
34
35        content = <View> {
36            width: Fit,
37            height: Fit,
38            spacing: 2,
39            circle1 = <EllipsisDot> {}
40            circle2 = <EllipsisDot> {}
41            circle3 = <EllipsisDot> {}
42        }
43
44        animator: {
45            circle1 = {
46                default: down,
47                down = {
48                    redraw: true,
49                    from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
50                    ease: InOutQuad
51                    apply: {content = { circle1 = { margin: {top: (DEFAULT_SWING_TOP)} }}}
52                }
53                up = {
54                    redraw: true,
55                    from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
56                    ease: InOutQuad
57                    apply: {content = { circle1 = { margin: {top: (DEFAULT_SWING_BOTTOM)} }}}
58                }
59            }
60
61            circle2 = {
62                default: down,
63                down = {
64                    redraw: true,
65                    from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
66                    ease: InOutQuad
67                    apply: {content = { circle2 = { margin: {top: (DEFAULT_SWING_TOP)} }}}
68                }
69                up = {
70                    redraw: true,
71                    from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
72                    ease: InOutQuad
73                    apply: {content = { circle2 = { margin: {top: (DEFAULT_SWING_BOTTOM)} }}}
74                }
75            }
76
77            circle3 = {
78                default: down,
79                down = {
80                    redraw: true,
81                    from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
82                    ease: InOutQuad
83                    apply: {content = { circle3 = { margin: {top: (DEFAULT_SWING_TOP)} }}}
84                }
85                up = {
86                    redraw: true,
87                    from: {all: Forward {duration: (ANIMATION_DURATION * 0.5)}}
88                    ease: InOutQuad
89                    apply: {content = { circle3 = { margin: {top: (DEFAULT_SWING_BOTTOM)} }}}
90                }
91            }
92        }
93    }
94}
95
96#[derive(Live, Widget)]
97pub struct TypingAnimation {
98    #[deref] view: View,
99    #[animator] animator: Animator,
100
101    #[live(0.65)] animation_duration: f64,
102    #[live(10.0)] swing_amplitude: f64,  // 摆动幅度
103    #[live(3.0)] swing_base: f64,       // 基础高度
104
105    #[rust] next_frame: Option<NextFrame>,
106    #[rust] animation_start_time: f64,
107    #[rust] current_animated_dot: CurrentAnimatedDot,
108
109    #[rust] initialized: bool, // 添加初始化标志
110    #[rust] is_animating: bool, // 添加动画状态标志
111}
112
113impl LiveHook for TypingAnimation {
114    fn after_apply(&mut self, cx: &mut Cx, apply: &mut Apply, index: usize, nodes: &[LiveNode]) {
115        // 在组件完全应用后初始化
116        if !self.initialized {
117            self.initialized = true;
118        }
119    }
120}
121
122#[derive(Copy, Clone, Default)]
123enum CurrentAnimatedDot {
124    #[default]
125    Dot1,
126    Dot2,
127    Dot3,
128}
129impl CurrentAnimatedDot {
130    fn next(&self) -> Self {
131        match self {
132            Self::Dot1 => Self::Dot2,
133            Self::Dot2 => Self::Dot3,
134            Self::Dot3 => Self::Dot1,
135        }
136    }
137}
138
139impl Widget for TypingAnimation {
140    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
141        // 先检查初始化
142        if self.initialized && !self.is_animating {
143            self.init(cx);
144        }
145
146        match event {
147            Event::NextFrame(ne) => {
148                // 检查是否是我们的 next_frame
149                if let Some(next_frame) = self.next_frame {
150                    if ne.set.contains(&next_frame) {
151                        let elapsed = ne.time - self.animation_start_time;
152                        if elapsed >= self.animation_duration * 0.5 {
153                            // 更新动画
154                            self.update_animation(cx, ne.time);
155                        }
156                        // 继续请求下一帧
157                        self.next_frame = Some(cx.new_next_frame());
158                    }
159                } else if self.initialized && !self.is_animating {
160                    // 如果组件已初始化但没有动画,重新开始动画
161                    self.animation_start_time = ne.time;
162                    self.next_frame = Some(cx.new_next_frame());
163                    self.update_animation(cx, ne.time);
164                    self.is_animating = true;
165                }
166            }
167            Event::WindowGeomChange(_) => {
168                self.reset_animation(cx);
169            }
170            _ => {}
171        }
172
173        if self.animator_handle_event(cx, event).must_redraw() {
174            self.redraw(cx);
175        }
176
177        self.view.handle_event(cx, event, scope);
178    }
179
180    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
181        self.view.draw_walk(cx, scope, walk)
182    }
183}
184
185impl TypingAnimation {
186
187    pub fn init(&mut self, cx: &mut Cx) {
188        self.animation_start_time = cx.seconds_since_app_start();
189        self.next_frame = Some(cx.new_next_frame());
190
191        if let Some(state) = &mut self.animator.state {
192            // 设置初始状态
193            state.push_live(live!{
194                state: {
195                    content: {
196                        circle1: { margin: { top: (self.swing_base) }},
197                        circle2: { margin: { top: (self.swing_base) }},
198                        circle3: { margin: { top: (self.swing_base) }}
199                    }
200                }
201            });
202        }
203
204        // 立即开始第一个动画
205        self.update_animation(cx, self.animation_start_time);
206        self.is_animating = true; // 设为 true,表示动画已经开始
207    }
208
209    fn reset_animation(&mut self, cx: &mut Cx) {
210        self.animation_start_time = cx.seconds_since_app_start();
211        self.next_frame = Some(cx.new_next_frame());
212        self.current_animated_dot = CurrentAnimatedDot::default();
213        self.is_animating = false;
214    }
215    pub fn update_animation(&mut self, cx: &mut Cx, time: f64) {
216        self.current_animated_dot = self.current_animated_dot.next();
217        self.animation_start_time = time;
218
219        match self.current_animated_dot {
220            CurrentAnimatedDot::Dot1 => {
221                // 构建状态节点
222                let mut up_nodes = LiveNodeVec::new();
223                up_nodes.push_live(live_object!{
224                    content: {
225                        circle1: {
226                            margin: {top: (self.swing_base)}
227                        }
228                    }
229                });
230
231                let mut down_nodes = LiveNodeVec::new();
232                down_nodes.push_live(live_object!{
233                    content: {
234                        circle3: {
235                            margin: {top: (self.swing_amplitude)}
236                        }
237                    }
238                });
239
240                // 更新动画状态
241                if let Some(state) = &mut self.animator.state {
242                    state.replace_or_insert_last_node_by_path(
243                        0,
244                        &[live_id!(circle1).as_field(), live_id!(up).as_field(), live_id!(apply).as_field()],
245                        &up_nodes
246                    );
247
248                    state.replace_or_insert_last_node_by_path(
249                        0,
250                        &[live_id!(circle3).as_field(), live_id!(down).as_field(), live_id!(apply).as_field()],
251                        &down_nodes
252                    );
253                }
254
255                self.animator_play(cx, id!(circle1.up));
256                self.animator_play(cx, id!(circle3.down));
257            }
258            CurrentAnimatedDot::Dot2 => {
259                let mut up_nodes = LiveNodeVec::new();
260                up_nodes.push_live(live_object!{
261                    content: {
262                        circle2: {
263                            margin: {top: (self.swing_base)}
264                        }
265                    }
266                });
267
268                let mut down_nodes = LiveNodeVec::new();
269                down_nodes.push_live(live_object!{
270                    content: {
271                        circle1: {
272                            margin: {top: (self.swing_amplitude)}
273                        }
274                    }
275                });
276
277                if let Some(state) = &mut self.animator.state {
278                    state.replace_or_insert_last_node_by_path(
279                        0,
280                        &[live_id!(circle2).as_field(), live_id!(up).as_field(), live_id!(apply).as_field()],
281                        &up_nodes
282                    );
283
284                    state.replace_or_insert_last_node_by_path(
285                        0,
286                        &[live_id!(circle1).as_field(), live_id!(down).as_field(), live_id!(apply).as_field()],
287                        &down_nodes
288                    );
289                }
290
291                self.animator_play(cx, id!(circle1.down));
292                self.animator_play(cx, id!(circle2.up));
293            }
294            CurrentAnimatedDot::Dot3 => {
295                let mut up_nodes = LiveNodeVec::new();
296                up_nodes.push_live(live_object!{
297                    content: {
298                        circle3: {
299                            margin: {top: (self.swing_base)}
300                        }
301                    }
302                });
303
304                let mut down_nodes = LiveNodeVec::new();
305                down_nodes.push_live(live_object!{
306                    content: {
307                        circle2: {
308                            margin: {top: (self.swing_amplitude)}
309                        }
310                    }
311                });
312
313                if let Some(state) = &mut self.animator.state {
314                    state.replace_or_insert_last_node_by_path(
315                        0,
316                        &[live_id!(circle3).as_field(), live_id!(up).as_field(), live_id!(apply).as_field()],
317                        &up_nodes
318                    );
319
320                    state.replace_or_insert_last_node_by_path(
321                        0,
322                        &[live_id!(circle2).as_field(), live_id!(down).as_field(), live_id!(apply).as_field()],
323                        &down_nodes
324                    );
325                }
326
327                self.animator_play(cx, id!(circle2.down));
328                self.animator_play(cx, id!(circle3.up));
329            }
330        };
331    }
332
333    pub fn start(&mut self, cx: &mut Cx) {
334        // 如果动画没在运行,就启动它
335        if !self.is_animating {
336            self.initialized = true;     // 确保初始化状态
337            self.is_animating = false;   // 临时设为 false,这样 handle_event 会重新启动动画
338            self.reset_animation(cx);    // 重置动画状态
339
340            // 强制立即开始一次动画循环
341            self.animation_start_time = cx.seconds_since_app_start();
342            self.next_frame = Some(cx.new_next_frame());
343            self.update_animation(cx, self.animation_start_time);
344        }
345    }
346
347    pub fn stop(&mut self, cx: &mut Cx) {
348        if self.is_animating {
349            self.is_animating = false;   // 停止动画循环
350            self.next_frame = None;      // 移除下一帧的请求
351
352            // 将所有点重置到基础位置
353            if let Some(state) = &mut self.animator.state {
354                state.push_live(live!{
355                    state: {
356                        content: {
357                            circle1: { margin: { top: (self.swing_base) }},
358                            circle2: { margin: { top: (self.swing_base) }},
359                            circle3: { margin: { top: (self.swing_base) }}
360                        }
361                    }
362                });
363            }
364            self.redraw(cx);  // 强制重绘以更新视觉状态
365        }
366    }
367
368}
369
370
371/// // 设置小幅度快速摆动
372/// typing_animation.set_swing_parameters(cx, 5.0, 2.0); // 小幅度
373/// typing_animation.set_animation_speed(cx, 0.3); // 更快的速度
374///
375/// // 设置大幅度慢速摆动
376/// typing_animation.set_swing_parameters(cx, 15.0, 3.0); // 大幅度
377/// typing_animation.set_animation_speed(cx, 1.0); // 更慢的速度
378impl TypingAnimationRef {
379    // 提供设置动画参数的方法
380    pub fn set_swing_parameters(&self, cx: &mut Cx, amplitude: f64, base: f64) {
381        if let Some(mut inner) = self.borrow_mut() {
382            inner.swing_amplitude = amplitude;
383            inner.swing_base = base;
384            // 如果动画正在进行,需要重新应用参数
385            if inner.next_frame.is_some() {
386                let time = cx.seconds_since_app_start();
387                inner.update_animation(cx, time);
388            }
389        }
390    }
391
392    pub fn set_animation_speed(&self, cx: &mut Cx, duration: f64) {
393        if let Some(mut inner) = self.borrow_mut() {
394            inner.animation_duration = duration;
395            // 如果动画正在进行,需要重新应用参数
396            if inner.next_frame.is_some() {
397                let time = cx.seconds_since_app_start();
398                inner.update_animation(cx, time);
399            }
400        }
401    }
402
403    pub fn start(&self, cx: &mut Cx) {
404        // self.set_swing_parameters(cx, 5.0, 2.0);
405        self.set_animation_speed(cx, 0.3);
406        if let Some(mut inner) = self.borrow_mut() {
407            inner.start(cx);
408        }
409    }
410
411    pub fn stop(&self, cx: &mut Cx) {
412        if let Some(mut inner) = self.borrow_mut() {
413            inner.stop(cx);
414        }
415    }
416}

使用方法

1let typing_animation = self.view.typing_animation(id!(typing_animation));
2// Start
3typing_animation.start(cx);
4// Stop
5typing_animation.stop(cx);
ON THIS PAGE