1use std::{
6 cell::{Cell, RefCell},
7 rc::Rc,
8};
9
10use cranpose_animation::{
11 ExponentialDecaySpec, FloatDecayAnimationSpec, IOS_DECELERATION_RATE_NORMAL,
12};
13use cranpose_core::{
14 RuntimeHandle,
15 internal::{FrameCallbackRegistration, FrameClock},
16};
17
18pub const MIN_FLING_VELOCITY: f32 = 300.0;
25
26const BOUNDARY_EPSILON: f32 = 0.5;
27
28fn schedule_next_frame<F, G>(
29 state: Rc<RefCell<Option<FlingAnimationState>>>,
30 frame_clock: FrameClock,
31 on_scroll: F,
32 on_end: G,
33) where
34 F: Fn(f32) -> f32 + 'static,
35 G: FnOnce() + 'static,
36{
37 let state_for_closure = state.clone();
38 let frame_clock_for_closure = frame_clock.clone();
39 let on_end = RefCell::new(Some(on_end));
40
41 let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
42 let should_continue = {
43 let state_guard = state_for_closure.borrow();
44 let Some(anim_state) = state_guard.as_ref() else {
45 return;
46 };
47
48 if !anim_state.is_running.get() {
49 return;
50 }
51
52 let start_time = match anim_state.start_frame_time_nanos.get() {
53 Some(value) => value,
54 None => {
55 anim_state
56 .start_frame_time_nanos
57 .set(Some(frame_time_nanos));
58 frame_time_nanos
59 }
60 };
61
62 let play_time_nanos = frame_time_nanos.saturating_sub(start_time) as i64;
63
64 let new_value = anim_state.decay_spec.get_value_from_nanos(
65 play_time_nanos,
66 anim_state.initial_value,
67 anim_state.initial_velocity,
68 );
69
70 let last = anim_state.last_value.get();
71 let delta = new_value - last;
72 anim_state.last_value.set(new_value);
73 anim_state
74 .total_delta
75 .set(anim_state.total_delta.get() + delta);
76
77 let duration_nanos = anim_state
78 .decay_spec
79 .get_duration_nanos(anim_state.initial_value, anim_state.initial_velocity);
80
81 let current_velocity = anim_state.decay_spec.get_velocity_from_nanos(
82 play_time_nanos,
83 anim_state.initial_value,
84 anim_state.initial_velocity,
85 );
86
87 let is_finished = play_time_nanos >= duration_nanos
88 || current_velocity.abs() < anim_state.decay_spec.abs_velocity_threshold();
89
90 if is_finished {
91 anim_state.is_running.set(false);
92 }
93
94 let consumed = if delta.abs() > 0.001 {
95 on_scroll(delta)
96 } else {
97 0.0
98 };
99
100 let boundary_hit = (delta - consumed).abs() > BOUNDARY_EPSILON;
101 if boundary_hit {
102 anim_state.is_running.set(false);
103 }
104
105 !is_finished && !boundary_hit
106 };
107
108 if should_continue {
109 if let Some(on_end_fn) = on_end.borrow_mut().take() {
110 schedule_next_frame(
111 state_for_closure.clone(),
112 frame_clock_for_closure.clone(),
113 on_scroll,
114 on_end_fn,
115 );
116 }
117 } else if let Some(end_fn) = on_end.borrow_mut().take() {
118 end_fn();
119 }
120 });
121
122 if let Some(anim_state) = state.borrow_mut().as_mut() {
123 anim_state.registration = Some(registration);
124 }
125}
126
127struct FlingAnimationState {
128 initial_value: f32,
129 last_value: Cell<f32>,
130 initial_velocity: f32,
131 start_frame_time_nanos: Cell<Option<u64>>,
132 decay_spec: ExponentialDecaySpec,
133 registration: Option<FrameCallbackRegistration>,
134 is_running: Cell<bool>,
135 total_delta: Cell<f32>,
136}
137
138pub struct FlingAnimation {
143 state: Rc<RefCell<Option<FlingAnimationState>>>,
144 frame_clock: FrameClock,
145}
146
147impl FlingAnimation {
148 pub fn new(runtime: RuntimeHandle) -> Self {
150 Self {
151 state: Rc::new(RefCell::new(None)),
152 frame_clock: runtime.frame_clock(),
153 }
154 }
155
156 pub fn start_fling<F, G>(&self, initial_value: f32, velocity: f32, on_scroll: F, on_end: G)
164 where
165 F: Fn(f32) -> f32 + 'static,
166 G: FnOnce() + 'static,
167 {
168 self.cancel();
169
170 if velocity.abs() < MIN_FLING_VELOCITY {
171 on_end();
172 return;
173 }
174
175 let decay_spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
176
177 let anim_state = FlingAnimationState {
178 initial_value,
179 last_value: Cell::new(initial_value),
180 initial_velocity: velocity,
181 start_frame_time_nanos: Cell::new(None),
182 decay_spec,
183 registration: None,
184 is_running: Cell::new(true),
185 total_delta: Cell::new(0.0),
186 };
187
188 *self.state.borrow_mut() = Some(anim_state);
189
190 schedule_next_frame(
191 self.state.clone(),
192 self.frame_clock.clone(),
193 on_scroll,
194 on_end,
195 );
196 }
197
198 pub fn cancel(&self) {
200 if let Some(state) = self.state.borrow_mut().take() {
201 state.is_running.set(false);
202 drop(state.registration);
203 }
204 }
205
206 pub fn is_running(&self) -> bool {
208 self.state
209 .borrow()
210 .as_ref()
211 .is_some_and(|s| s.is_running.get())
212 }
213}
214
215impl Clone for FlingAnimation {
216 fn clone(&self) -> Self {
217 Self {
218 state: self.state.clone(),
219 frame_clock: self.frame_clock.clone(),
220 }
221 }
222}
223
224pub fn fling_rest_position(initial_value: f32, velocity: f32) -> f32 {
230 if velocity.abs() < MIN_FLING_VELOCITY {
231 return initial_value;
232 }
233 let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
234 spec.get_target_value(initial_value, velocity)
235}
236
237#[derive(Debug, Clone, Copy)]
241pub struct SpringParams {
242 pub stiffness: f32,
244 pub damping_ratio: f32,
247}
248
249impl SpringParams {
250 pub const SETTLE_POLICY: Self = Self {
254 stiffness: 300.0,
255 damping_ratio: 1.0,
256 };
257
258 pub const OVERSCROLL_BOUNCE: Self = Self {
269 stiffness: 1909.69,
270 damping_ratio: 2.71,
271 };
272}
273
274const SETTLE_REST_DISTANCE: f32 = 0.1;
275const SETTLE_REST_VELOCITY: f32 = 4.0;
276
277struct SettleAnimationState {
278 displacement: Cell<f32>,
281 velocity: Cell<f32>,
282 params: SpringParams,
283 last_frame_time_nanos: Cell<Option<u64>>,
284 registration: Option<FrameCallbackRegistration>,
285 is_running: Cell<bool>,
286}
287
288pub(crate) struct SettleEnd {
289 pub(crate) velocity: f32,
290 pub(crate) hit_boundary: bool,
291}
292
293pub struct SettleAnimation {
298 state: Rc<RefCell<Option<SettleAnimationState>>>,
299 frame_clock: FrameClock,
300 params: SpringParams,
301}
302
303impl SettleAnimation {
304 pub fn new(runtime: RuntimeHandle, params: SpringParams) -> Self {
307 Self {
308 state: Rc::new(RefCell::new(None)),
309 frame_clock: runtime.frame_clock(),
310 params,
311 }
312 }
313
314 pub(crate) fn start_settle<F, G>(
315 &self,
316 initial_value: f32,
317 initial_velocity: f32,
318 target: f32,
319 on_scroll: F,
320 on_end: G,
321 ) where
322 F: Fn(f32) -> f32 + 'static,
323 G: FnOnce(SettleEnd) + 'static,
324 {
325 self.cancel();
326 *self.state.borrow_mut() = Some(SettleAnimationState {
327 displacement: Cell::new(initial_value - target),
328 velocity: Cell::new(initial_velocity),
329 params: self.params,
330 last_frame_time_nanos: Cell::new(None),
331 registration: None,
332 is_running: Cell::new(true),
333 });
334 schedule_next_settle_frame(
335 self.state.clone(),
336 self.frame_clock.clone(),
337 on_scroll,
338 on_end,
339 );
340 }
341
342 pub fn cancel(&self) {
344 if let Some(state) = self.state.borrow_mut().take() {
345 state.is_running.set(false);
346 drop(state.registration);
347 }
348 }
349
350 pub fn is_running(&self) -> bool {
352 self.state
353 .borrow()
354 .as_ref()
355 .is_some_and(|s| s.is_running.get())
356 }
357}
358
359impl Clone for SettleAnimation {
360 fn clone(&self) -> Self {
361 Self {
362 state: self.state.clone(),
363 frame_clock: self.frame_clock.clone(),
364 params: self.params,
365 }
366 }
367}
368
369fn schedule_next_settle_frame<F, G>(
370 state: Rc<RefCell<Option<SettleAnimationState>>>,
371 frame_clock: FrameClock,
372 on_scroll: F,
373 on_end: G,
374) where
375 F: Fn(f32) -> f32 + 'static,
376 G: FnOnce(SettleEnd) + 'static,
377{
378 let state_for_closure = state.clone();
379 let frame_clock_for_closure = frame_clock.clone();
380 let on_end = RefCell::new(Some(on_end));
381 let hit_boundary = Cell::new(false);
382
383 let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
384 let should_continue = {
385 let state_guard = state_for_closure.borrow();
386 let Some(anim_state) = state_guard.as_ref() else {
387 return;
388 };
389 if !anim_state.is_running.get() {
390 return;
391 }
392
393 let dt = match anim_state.last_frame_time_nanos.get() {
394 Some(last) => (frame_time_nanos.saturating_sub(last) as f32) / 1_000_000_000.0,
395 None => 0.0,
396 };
397 anim_state.last_frame_time_nanos.set(Some(frame_time_nanos));
398
399 let displacement = anim_state.displacement.get();
400 let (mut next_displacement, next_velocity) =
401 cranpose_animation::advance_spring_displacement(
402 displacement,
403 anim_state.velocity.get(),
404 anim_state.params.damping_ratio,
405 anim_state.params.stiffness,
406 dt.max(0.0),
407 );
408
409 let is_finished = next_displacement.abs() < SETTLE_REST_DISTANCE
410 && next_velocity.abs() < SETTLE_REST_VELOCITY;
411 if is_finished {
412 next_displacement = 0.0;
413 anim_state.is_running.set(false);
414 }
415
416 let delta = next_displacement - displacement;
417 anim_state.displacement.set(next_displacement);
418 anim_state.velocity.set(next_velocity);
419
420 let consumed = if delta.abs() > 0.0001 {
421 on_scroll(delta)
422 } else {
423 delta
424 };
425 let boundary_hit = (delta - consumed).abs() > BOUNDARY_EPSILON;
426 if boundary_hit {
427 anim_state.is_running.set(false);
428 hit_boundary.set(true);
429 }
430
431 !is_finished && !boundary_hit
432 };
433
434 if should_continue {
435 if let Some(on_end_fn) = on_end.borrow_mut().take() {
436 schedule_next_settle_frame(
437 state_for_closure.clone(),
438 frame_clock_for_closure.clone(),
439 on_scroll,
440 on_end_fn,
441 );
442 }
443 } else if let Some(end_fn) = on_end.borrow_mut().take() {
444 let state_guard = state_for_closure.borrow();
445 let velocity = state_guard
446 .as_ref()
447 .map_or(0.0, |anim_state| anim_state.velocity.get());
448 end_fn(SettleEnd {
449 velocity,
450 hit_boundary: hit_boundary.get(),
451 });
452 }
453 });
454
455 if let Some(anim_state) = state.borrow_mut().as_mut() {
456 anim_state.registration = Some(registration);
457 }
458}
459
460#[cfg(test)]
461#[path = "tests/fling_animation_tests.rs"]
462mod tests;