Complete Animation Example

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        // Add configurable animation parameters
28        swing_amplitude: (DEFAULT_SWING_TOP), // Maximum height of ball swing
29        swing_base: (DEFAULT_SWING_BOTTOM),   // Base height of ball swing 
30        animation_duration: (ANIMATION_DURATION), // Animation cycle
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        if !self.initialized {
116            self.initialized = true;
117        }
118    }
119}
120
121#[derive(Copy, Clone, Default)]
122enum CurrentAnimatedDot {
123    #[default]
124    Dot1,
125    Dot2,
126    Dot3,
127}
128impl CurrentAnimatedDot {
129    fn next(&self) -> Self {
130        match self {
131            Self::Dot1 => Self::Dot2,
132            Self::Dot2 => Self::Dot3,
133            Self::Dot3 => Self::Dot1,
134        }
135    }
136}
137
138impl Widget for TypingAnimation {
139    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
140        if self.initialized && !self.is_animating {
141            self.init(cx);
142        }
143
144        match event {
145            Event::NextFrame(ne) => {
146                if let Some(next_frame) = self.next_frame {
147                    if ne.set.contains(&next_frame) {
148                        let elapsed = ne.time - self.animation_start_time;
149                        if elapsed >= self.animation_duration * 0.5 {
150                            self.update_animation(cx, ne.time);
151                        }
152                        // Continue to request the next frame
153                        self.next_frame = Some(cx.new_next_frame());
154                    }
155                } else if self.initialized && !self.is_animating {
156                    //  If the component is initialized but has no animation, restart the animation
157                    self.animation_start_time = ne.time;
158                    self.next_frame = Some(cx.new_next_frame());
159                    self.update_animation(cx, ne.time);
160                    self.is_animating = true;
161                }
162            }
163            Event::WindowGeomChange(_) => {
164                self.reset_animation(cx);
165            }
166            _ => {}
167        }
168
169        if self.animator_handle_event(cx, event).must_redraw() {
170            self.redraw(cx);
171        }
172
173        self.view.handle_event(cx, event, scope);
174    }
175
176    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
177        self.view.draw_walk(cx, scope, walk)
178    }
179}
180
181impl TypingAnimation {
182
183    pub fn init(&mut self, cx: &mut Cx) {
184        self.animation_start_time = cx.seconds_since_app_start();
185        self.next_frame = Some(cx.new_next_frame());
186
187        if let Some(state) = &mut self.animator.state {
188            state.push_live(live!{
189                state: {
190                    content: {
191                        circle1: { margin: { top: (self.swing_base) }},
192                        circle2: { margin: { top: (self.swing_base) }},
193                        circle3: { margin: { top: (self.swing_base) }}
194                    }
195                }
196            });
197        }
198
199        self.update_animation(cx, self.animation_start_time);
200        self.is_animating = true; 
201    }
202
203    fn reset_animation(&mut self, cx: &mut Cx) {
204        self.animation_start_time = cx.seconds_since_app_start();
205        self.next_frame = Some(cx.new_next_frame());
206        self.current_animated_dot = CurrentAnimatedDot::default();
207        self.is_animating = false;
208    }
209    pub fn update_animation(&mut self, cx: &mut Cx, time: f64) {
210        self.current_animated_dot = self.current_animated_dot.next();
211        self.animation_start_time = time;
212
213        match self.current_animated_dot {
214            CurrentAnimatedDot::Dot1 => {
215                let mut up_nodes = LiveNodeVec::new();
216                up_nodes.push_live(live_object!{
217                    content: {
218                        circle1: {
219                            margin: {top: (self.swing_base)}
220                        }
221                    }
222                });
223
224                let mut down_nodes = LiveNodeVec::new();
225                down_nodes.push_live(live_object!{
226                    content: {
227                        circle3: {
228                            margin: {top: (self.swing_amplitude)}
229                        }
230                    }
231                });
232
233                if let Some(state) = &mut self.animator.state {
234                    state.replace_or_insert_last_node_by_path(
235                        0,
236                        &[live_id!(circle1).as_field(), live_id!(up).as_field(), live_id!(apply).as_field()],
237                        &up_nodes
238                    );
239
240                    state.replace_or_insert_last_node_by_path(
241                        0,
242                        &[live_id!(circle3).as_field(), live_id!(down).as_field(), live_id!(apply).as_field()],
243                        &down_nodes
244                    );
245                }
246
247                self.animator_play(cx, id!(circle1.up));
248                self.animator_play(cx, id!(circle3.down));
249            }
250            CurrentAnimatedDot::Dot2 => {
251                let mut up_nodes = LiveNodeVec::new();
252                up_nodes.push_live(live_object!{
253                    content: {
254                        circle2: {
255                            margin: {top: (self.swing_base)}
256                        }
257                    }
258                });
259
260                let mut down_nodes = LiveNodeVec::new();
261                down_nodes.push_live(live_object!{
262                    content: {
263                        circle1: {
264                            margin: {top: (self.swing_amplitude)}
265                        }
266                    }
267                });
268
269                if let Some(state) = &mut self.animator.state {
270                    state.replace_or_insert_last_node_by_path(
271                        0,
272                        &[live_id!(circle2).as_field(), live_id!(up).as_field(), live_id!(apply).as_field()],
273                        &up_nodes
274                    );
275
276                    state.replace_or_insert_last_node_by_path(
277                        0,
278                        &[live_id!(circle1).as_field(), live_id!(down).as_field(), live_id!(apply).as_field()],
279                        &down_nodes
280                    );
281                }
282
283                self.animator_play(cx, id!(circle1.down));
284                self.animator_play(cx, id!(circle2.up));
285            }
286            CurrentAnimatedDot::Dot3 => {
287                let mut up_nodes = LiveNodeVec::new();
288                up_nodes.push_live(live_object!{
289                    content: {
290                        circle3: {
291                            margin: {top: (self.swing_base)}
292                        }
293                    }
294                });
295
296                let mut down_nodes = LiveNodeVec::new();
297                down_nodes.push_live(live_object!{
298                    content: {
299                        circle2: {
300                            margin: {top: (self.swing_amplitude)}
301                        }
302                    }
303                });
304
305                if let Some(state) = &mut self.animator.state {
306                    state.replace_or_insert_last_node_by_path(
307                        0,
308                        &[live_id!(circle3).as_field(), live_id!(up).as_field(), live_id!(apply).as_field()],
309                        &up_nodes
310                    );
311
312                    state.replace_or_insert_last_node_by_path(
313                        0,
314                        &[live_id!(circle2).as_field(), live_id!(down).as_field(), live_id!(apply).as_field()],
315                        &down_nodes
316                    );
317                }
318
319                self.animator_play(cx, id!(circle2.down));
320                self.animator_play(cx, id!(circle3.up));
321            }
322        };
323    }
324
325    pub fn start(&mut self, cx: &mut Cx) {
326        if !self.is_animating {
327            self.initialized = true;   
328            self.is_animating = false;  
329            self.reset_animation(cx);  
330            self.animation_start_time = cx.seconds_since_app_start();
331            self.next_frame = Some(cx.new_next_frame());
332            self.update_animation(cx, self.animation_start_time);
333        }
334    }
335
336    pub fn stop(&mut self, cx: &mut Cx) {
337        if self.is_animating {
338            self.is_animating = false;  
339            self.next_frame = None;    
340
341            if let Some(state) = &mut self.animator.state {
342                state.push_live(live!{
343                    state: {
344                        content: {
345                            circle1: { margin: { top: (self.swing_base) }},
346                            circle2: { margin: { top: (self.swing_base) }},
347                            circle3: { margin: { top: (self.swing_base) }}
348                        }
349                    }
350                });
351            }
352            self.redraw(cx); 
353        }
354    }
355
356}
357
358
359
360/// typing_animation.set_swing_parameters(cx, 5.0, 2.0); 
361/// typing_animation.set_animation_speed(cx, 0.3); 
362///
363/// 
364/// typing_animation.set_swing_parameters(cx, 15.0, 3.0); 
365/// typing_animation.set_animation_speed(cx, 1.0); 
366impl TypingAnimationRef {
367
368    pub fn set_swing_parameters(&self, cx: &mut Cx, amplitude: f64, base: f64) {
369        if let Some(mut inner) = self.borrow_mut() {
370            inner.swing_amplitude = amplitude;
371            inner.swing_base = base;
372
373            if inner.next_frame.is_some() {
374                let time = cx.seconds_since_app_start();
375                inner.update_animation(cx, time);
376            }
377        }
378    }
379
380    pub fn set_animation_speed(&self, cx: &mut Cx, duration: f64) {
381        if let Some(mut inner) = self.borrow_mut() {
382            inner.animation_duration = duration;
383
384            if inner.next_frame.is_some() {
385                let time = cx.seconds_since_app_start();
386                inner.update_animation(cx, time);
387            }
388        }
389    }
390
391    pub fn start(&self, cx: &mut Cx) {
392        // self.set_swing_parameters(cx, 5.0, 2.0);
393        self.set_animation_speed(cx, 0.3);
394        if let Some(mut inner) = self.borrow_mut() {
395            inner.start(cx);
396        }
397    }
398
399    pub fn stop(&self, cx: &mut Cx) {
400        if let Some(mut inner) = self.borrow_mut() {
401            inner.stop(cx);
402        }
403    }
404}

Usage:

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