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;
28
29fn schedule_next_frame<F, G>(
32 state: Rc<RefCell<Option<FlingAnimationState>>>,
33 frame_clock: FrameClock,
34 on_scroll: F,
35 on_end: G,
36) where
37 F: Fn(f32) -> f32 + 'static,
38 G: FnOnce() + 'static,
39{
40 let state_for_closure = state.clone();
41 let frame_clock_for_closure = frame_clock.clone();
42 let on_end = RefCell::new(Some(on_end));
43
44 let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
45 let should_continue = {
46 let state_guard = state_for_closure.borrow();
47 let Some(anim_state) = state_guard.as_ref() else {
48 return;
49 };
50
51 if !anim_state.is_running.get() {
52 return;
53 }
54
55 let start_time = match anim_state.start_frame_time_nanos.get() {
56 Some(value) => value,
57 None => {
58 anim_state
59 .start_frame_time_nanos
60 .set(Some(frame_time_nanos));
61 frame_time_nanos
62 }
63 };
64
65 let play_time_nanos = frame_time_nanos.saturating_sub(start_time) as i64;
66
67 let new_value = anim_state.decay_spec.get_value_from_nanos(
68 play_time_nanos,
69 anim_state.initial_value,
70 anim_state.initial_velocity,
71 );
72
73 let last = anim_state.last_value.get();
74 let delta = new_value - last;
75 anim_state.last_value.set(new_value);
76 anim_state
77 .total_delta
78 .set(anim_state.total_delta.get() + delta);
79
80 let duration_nanos = anim_state
81 .decay_spec
82 .get_duration_nanos(anim_state.initial_value, anim_state.initial_velocity);
83
84 let current_velocity = anim_state.decay_spec.get_velocity_from_nanos(
85 play_time_nanos,
86 anim_state.initial_value,
87 anim_state.initial_velocity,
88 );
89
90 let is_finished = play_time_nanos >= duration_nanos
91 || current_velocity.abs() < anim_state.decay_spec.abs_velocity_threshold();
92
93 if is_finished {
94 anim_state.is_running.set(false);
95 }
96
97 let consumed = if delta.abs() > 0.001 {
98 on_scroll(delta)
99 } else {
100 0.0
101 };
102
103 let boundary_hit = (delta - consumed).abs() > BOUNDARY_EPSILON;
104 if boundary_hit {
105 anim_state.is_running.set(false);
106 }
107
108 !is_finished && !boundary_hit
109 };
110
111 if should_continue {
112 if let Some(on_end_fn) = on_end.borrow_mut().take() {
113 schedule_next_frame(
114 state_for_closure.clone(),
115 frame_clock_for_closure.clone(),
116 on_scroll,
117 on_end_fn,
118 );
119 }
120 } else if let Some(end_fn) = on_end.borrow_mut().take() {
121 end_fn();
122 }
123 });
124
125 if let Some(anim_state) = state.borrow_mut().as_mut() {
127 anim_state.registration = Some(registration);
128 }
129}
130
131struct FlingAnimationState {
133 initial_value: f32,
135 last_value: Cell<f32>,
137 initial_velocity: f32,
139 start_frame_time_nanos: Cell<Option<u64>>,
141 decay_spec: ExponentialDecaySpec,
143 registration: Option<FrameCallbackRegistration>,
145 is_running: Cell<bool>,
147 total_delta: Cell<f32>,
149}
150
151pub struct FlingAnimation {
156 state: Rc<RefCell<Option<FlingAnimationState>>>,
157 frame_clock: FrameClock,
158}
159
160impl FlingAnimation {
161 pub fn new(runtime: RuntimeHandle) -> Self {
163 Self {
164 state: Rc::new(RefCell::new(None)),
165 frame_clock: runtime.frame_clock(),
166 }
167 }
168
169 pub fn start_fling<F, G>(&self, initial_value: f32, velocity: f32, on_scroll: F, on_end: G)
177 where
178 F: Fn(f32) -> f32 + 'static, G: FnOnce() + 'static,
180 {
181 self.cancel();
183
184 if velocity.abs() < MIN_FLING_VELOCITY {
186 on_end();
187 return;
188 }
189
190 let decay_spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
191
192 let anim_state = FlingAnimationState {
193 initial_value,
194 last_value: Cell::new(initial_value),
195 initial_velocity: velocity,
196 start_frame_time_nanos: Cell::new(None),
197 decay_spec,
198 registration: None,
199 is_running: Cell::new(true),
200 total_delta: Cell::new(0.0),
201 };
202
203 *self.state.borrow_mut() = Some(anim_state);
204
205 schedule_next_frame(
207 self.state.clone(),
208 self.frame_clock.clone(),
209 on_scroll,
210 on_end,
211 );
212 }
213
214 pub fn cancel(&self) {
215 if let Some(state) = self.state.borrow_mut().take() {
216 state.is_running.set(false);
218 drop(state.registration);
220 }
221 }
222
223 pub fn is_running(&self) -> bool {
225 self.state
226 .borrow()
227 .as_ref()
228 .is_some_and(|s| s.is_running.get())
229 }
230}
231
232impl Clone for FlingAnimation {
233 fn clone(&self) -> Self {
234 Self {
235 state: self.state.clone(),
236 frame_clock: self.frame_clock.clone(),
237 }
238 }
239}
240
241pub fn fling_rest_position(initial_value: f32, velocity: f32) -> f32 {
247 if velocity.abs() < MIN_FLING_VELOCITY {
248 return initial_value;
249 }
250 let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
251 spec.get_target_value(initial_value, velocity)
252}
253
254#[derive(Debug, Clone, Copy)]
258pub struct SpringParams {
259 pub stiffness: f32,
260 pub damping_ratio: f32,
261}
262
263impl SpringParams {
264 pub const SETTLE_POLICY: Self = Self {
268 stiffness: 300.0,
269 damping_ratio: 1.0,
270 };
271
272 pub const OVERSCROLL_BOUNCE: Self = Self {
283 stiffness: 1909.69,
284 damping_ratio: 2.71,
285 };
286}
287
288const SETTLE_REST_DISTANCE: f32 = 0.1;
290const SETTLE_REST_VELOCITY: f32 = 4.0;
291
292struct SettleAnimationState {
293 value: Cell<f32>,
294 velocity: Cell<f32>,
295 target: f32,
296 params: SpringParams,
297 last_frame_time_nanos: Cell<Option<u64>>,
298 registration: Option<FrameCallbackRegistration>,
299 is_running: Cell<bool>,
300}
301
302pub(crate) struct SettleEnd {
303 pub(crate) velocity: f32,
304 pub(crate) hit_boundary: bool,
305}
306
307pub struct SettleAnimation {
312 state: Rc<RefCell<Option<SettleAnimationState>>>,
313 frame_clock: FrameClock,
314 params: SpringParams,
315}
316
317impl SettleAnimation {
318 pub fn new(runtime: RuntimeHandle, params: SpringParams) -> Self {
319 Self {
320 state: Rc::new(RefCell::new(None)),
321 frame_clock: runtime.frame_clock(),
322 params,
323 }
324 }
325
326 pub(crate) fn start_settle<F, G>(
331 &self,
332 initial_value: f32,
333 initial_velocity: f32,
334 target: f32,
335 on_scroll: F,
336 on_end: G,
337 ) where
338 F: Fn(f32) -> f32 + 'static,
339 G: FnOnce(SettleEnd) + 'static,
340 {
341 self.cancel();
342 *self.state.borrow_mut() = Some(SettleAnimationState {
343 value: Cell::new(initial_value),
344 velocity: Cell::new(initial_velocity),
345 target,
346 params: self.params,
347 last_frame_time_nanos: Cell::new(None),
348 registration: None,
349 is_running: Cell::new(true),
350 });
351 schedule_next_settle_frame(
352 self.state.clone(),
353 self.frame_clock.clone(),
354 on_scroll,
355 on_end,
356 );
357 }
358
359 pub fn cancel(&self) {
360 if let Some(state) = self.state.borrow_mut().take() {
361 state.is_running.set(false);
362 drop(state.registration);
363 }
364 }
365
366 pub fn is_running(&self) -> bool {
367 self.state
368 .borrow()
369 .as_ref()
370 .is_some_and(|s| s.is_running.get())
371 }
372}
373
374impl Clone for SettleAnimation {
375 fn clone(&self) -> Self {
376 Self {
377 state: self.state.clone(),
378 frame_clock: self.frame_clock.clone(),
379 params: self.params,
380 }
381 }
382}
383
384fn schedule_next_settle_frame<F, G>(
385 state: Rc<RefCell<Option<SettleAnimationState>>>,
386 frame_clock: FrameClock,
387 on_scroll: F,
388 on_end: G,
389) where
390 F: Fn(f32) -> f32 + 'static,
391 G: FnOnce(SettleEnd) + 'static,
392{
393 let state_for_closure = state.clone();
394 let frame_clock_for_closure = frame_clock.clone();
395 let on_end = RefCell::new(Some(on_end));
396 let hit_boundary = Cell::new(false);
397
398 let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
399 let should_continue = {
400 let state_guard = state_for_closure.borrow();
401 let Some(anim_state) = state_guard.as_ref() else {
402 return;
403 };
404 if !anim_state.is_running.get() {
405 return;
406 }
407
408 let dt = match anim_state.last_frame_time_nanos.get() {
409 Some(last) => (frame_time_nanos.saturating_sub(last) as f32) / 1_000_000_000.0,
410 None => 0.0,
411 };
412 anim_state.last_frame_time_nanos.set(Some(frame_time_nanos));
413
414 let (mut next_value, next_velocity) = cranpose_animation::advance_spring(
415 anim_state.value.get(),
416 anim_state.velocity.get(),
417 anim_state.target,
418 anim_state.params.damping_ratio,
419 anim_state.params.stiffness,
420 dt.max(0.0),
421 );
422
423 let is_finished = (next_value - anim_state.target).abs() < SETTLE_REST_DISTANCE
424 && next_velocity.abs() < SETTLE_REST_VELOCITY;
425 if is_finished {
426 next_value = anim_state.target;
427 anim_state.is_running.set(false);
428 }
429
430 let delta = next_value - anim_state.value.get();
431 anim_state.value.set(next_value);
432 anim_state.velocity.set(next_velocity);
433
434 let consumed = if delta.abs() > 0.0001 {
435 on_scroll(delta)
436 } else {
437 delta
438 };
439 let boundary_hit = (delta - consumed).abs() > BOUNDARY_EPSILON;
440 if boundary_hit {
441 anim_state.is_running.set(false);
442 hit_boundary.set(true);
443 }
444
445 !is_finished && !boundary_hit
446 };
447
448 if should_continue {
449 if let Some(on_end_fn) = on_end.borrow_mut().take() {
450 schedule_next_settle_frame(
451 state_for_closure.clone(),
452 frame_clock_for_closure.clone(),
453 on_scroll,
454 on_end_fn,
455 );
456 }
457 } else if let Some(end_fn) = on_end.borrow_mut().take() {
458 let state_guard = state_for_closure.borrow();
459 let velocity = state_guard
460 .as_ref()
461 .map_or(0.0, |anim_state| anim_state.velocity.get());
462 end_fn(SettleEnd {
463 velocity,
464 hit_boundary: hit_boundary.get(),
465 });
466 }
467 });
468
469 if let Some(anim_state) = state.borrow_mut().as_mut() {
470 anim_state.registration = Some(registration);
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use std::{cell::Cell, rc::Rc, sync::Arc};
477
478 use cranpose_core::{DefaultScheduler, Runtime};
479
480 use super::*;
481
482 #[test]
483 fn test_min_velocity_threshold() {
484 assert_eq!(MIN_FLING_VELOCITY, 300.0);
485 }
486
487 #[test]
488 fn settle_animation_springs_to_target_and_ends() {
489 let runtime = Runtime::new(Arc::new(DefaultScheduler));
490 let handle = runtime.handle();
491 let settle = SettleAnimation::new(handle.clone(), SpringParams::SETTLE_POLICY);
492 let position = Rc::new(Cell::new(30.0f32));
493 let ended = Rc::new(Cell::new(false));
494 let position_for_scroll = Rc::clone(&position);
495 let ended_for_end = Rc::clone(&ended);
496 settle.start_settle(
497 30.0,
498 0.0,
499 52.0,
500 move |delta| {
501 position_for_scroll.set(position_for_scroll.get() + delta);
502 delta
503 },
504 move |_| ended_for_end.set(true),
505 );
506 for frame in 0..240u64 {
507 handle.drain_frame_callbacks(frame * 16_000_000);
508 if ended.get() {
509 break;
510 }
511 }
512 assert!(ended.get(), "settle animation must finish");
513 assert!(
514 (position.get() - 52.0).abs() < 0.2,
515 "settle must land on the target, got {}",
516 position.get()
517 );
518 }
519
520 #[test]
521 fn settle_reports_reduced_velocity_when_crossing_boundary() {
522 let runtime = Runtime::new(Arc::new(DefaultScheduler));
523 let handle = runtime.handle();
524 let settle = SettleAnimation::new(handle.clone(), SpringParams::SETTLE_POLICY);
525 let position = Rc::new(Cell::new(30.0f32));
526 let ended = Rc::new(Cell::new(None::<(f32, bool)>));
527 let position_for_scroll = Rc::clone(&position);
528 let ended_for_end = Rc::clone(&ended);
529 settle.start_settle(
530 30.0,
531 -1_200.0,
532 0.0,
533 move |delta| {
534 let previous = position_for_scroll.get();
535 let next = (previous + delta).max(0.0);
536 position_for_scroll.set(next);
537 next - previous
538 },
539 move |end| ended_for_end.set(Some((end.velocity, end.hit_boundary))),
540 );
541 for frame in 0..240u64 {
542 handle.drain_frame_callbacks(frame * 16_000_000);
543 if ended.get().is_some() {
544 break;
545 }
546 }
547
548 let (velocity, hit_boundary) = ended.get().expect("settle must finish");
549 assert!(hit_boundary);
550 assert!(velocity < 0.0 && velocity.abs() < 1_200.0);
551 assert_eq!(position.get(), 0.0);
552 }
553
554 #[test]
555 fn fling_rest_position_is_beyond_start_in_fling_direction() {
556 let rest = fling_rest_position(100.0, 900.0);
557 assert!(rest > 100.0, "rest {rest} must be past the start");
558 assert_eq!(fling_rest_position(100.0, 0.0), 100.0);
559 }
560
561 #[test]
562 fn test_on_end_called_when_boundary_hit() {
563 let runtime = Runtime::new(Arc::new(DefaultScheduler));
564 let handle = runtime.handle();
565 let fling = FlingAnimation::new(handle.clone());
566 let finished = Rc::new(Cell::new(false));
567 let finished_flag = Rc::clone(&finished);
568
569 fling.start_fling(0.0, 10_000.0, |_| 0.0, move || finished_flag.set(true));
570
571 handle.drain_frame_callbacks(0);
572 handle.drain_frame_callbacks(16_000_000);
573
574 assert!(finished.get());
575 }
576}