use std::{
cell::{Cell, RefCell},
rc::Rc,
};
use cranpose_animation::{
ExponentialDecaySpec, FloatDecayAnimationSpec, IOS_DECELERATION_RATE_NORMAL,
};
use cranpose_core::{
RuntimeHandle,
internal::{FrameCallbackRegistration, FrameClock},
};
pub const MIN_FLING_VELOCITY: f32 = 300.0;
const BOUNDARY_EPSILON: f32 = 0.5;
fn schedule_next_frame<F, G>(
state: Rc<RefCell<Option<FlingAnimationState>>>,
frame_clock: FrameClock,
on_scroll: F,
on_end: G,
) where
F: Fn(f32) -> f32 + 'static,
G: FnOnce() + 'static,
{
let state_for_closure = state.clone();
let frame_clock_for_closure = frame_clock.clone();
let on_end = RefCell::new(Some(on_end));
let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
let should_continue = {
let state_guard = state_for_closure.borrow();
let Some(anim_state) = state_guard.as_ref() else {
return;
};
if !anim_state.is_running.get() {
return;
}
let start_time = match anim_state.start_frame_time_nanos.get() {
Some(value) => value,
None => {
anim_state
.start_frame_time_nanos
.set(Some(frame_time_nanos));
frame_time_nanos
}
};
let play_time_nanos = frame_time_nanos.saturating_sub(start_time) as i64;
let new_value = anim_state.decay_spec.get_value_from_nanos(
play_time_nanos,
anim_state.initial_value,
anim_state.initial_velocity,
);
let last = anim_state.last_value.get();
let delta = new_value - last;
anim_state.last_value.set(new_value);
anim_state
.total_delta
.set(anim_state.total_delta.get() + delta);
let duration_nanos = anim_state
.decay_spec
.get_duration_nanos(anim_state.initial_value, anim_state.initial_velocity);
let current_velocity = anim_state.decay_spec.get_velocity_from_nanos(
play_time_nanos,
anim_state.initial_value,
anim_state.initial_velocity,
);
let is_finished = play_time_nanos >= duration_nanos
|| current_velocity.abs() < anim_state.decay_spec.abs_velocity_threshold();
if is_finished {
anim_state.is_running.set(false);
}
let consumed = if delta.abs() > 0.001 {
on_scroll(delta)
} else {
0.0
};
let boundary_hit = (delta - consumed).abs() > BOUNDARY_EPSILON;
if boundary_hit {
anim_state.is_running.set(false);
}
!is_finished && !boundary_hit
};
if should_continue {
if let Some(on_end_fn) = on_end.borrow_mut().take() {
schedule_next_frame(
state_for_closure.clone(),
frame_clock_for_closure.clone(),
on_scroll,
on_end_fn,
);
}
} else if let Some(end_fn) = on_end.borrow_mut().take() {
end_fn();
}
});
if let Some(anim_state) = state.borrow_mut().as_mut() {
anim_state.registration = Some(registration);
}
}
struct FlingAnimationState {
initial_value: f32,
last_value: Cell<f32>,
initial_velocity: f32,
start_frame_time_nanos: Cell<Option<u64>>,
decay_spec: ExponentialDecaySpec,
registration: Option<FrameCallbackRegistration>,
is_running: Cell<bool>,
total_delta: Cell<f32>,
}
pub struct FlingAnimation {
state: Rc<RefCell<Option<FlingAnimationState>>>,
frame_clock: FrameClock,
}
impl FlingAnimation {
pub fn new(runtime: RuntimeHandle) -> Self {
Self {
state: Rc::new(RefCell::new(None)),
frame_clock: runtime.frame_clock(),
}
}
pub fn start_fling<F, G>(&self, initial_value: f32, velocity: f32, on_scroll: F, on_end: G)
where
F: Fn(f32) -> f32 + 'static,
G: FnOnce() + 'static,
{
self.cancel();
if velocity.abs() < MIN_FLING_VELOCITY {
on_end();
return;
}
let decay_spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
let anim_state = FlingAnimationState {
initial_value,
last_value: Cell::new(initial_value),
initial_velocity: velocity,
start_frame_time_nanos: Cell::new(None),
decay_spec,
registration: None,
is_running: Cell::new(true),
total_delta: Cell::new(0.0),
};
*self.state.borrow_mut() = Some(anim_state);
schedule_next_frame(
self.state.clone(),
self.frame_clock.clone(),
on_scroll,
on_end,
);
}
pub fn cancel(&self) {
if let Some(state) = self.state.borrow_mut().take() {
state.is_running.set(false);
drop(state.registration);
}
}
pub fn is_running(&self) -> bool {
self.state
.borrow()
.as_ref()
.is_some_and(|s| s.is_running.get())
}
}
impl Clone for FlingAnimation {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
frame_clock: self.frame_clock.clone(),
}
}
}
pub fn fling_rest_position(initial_value: f32, velocity: f32) -> f32 {
if velocity.abs() < MIN_FLING_VELOCITY {
return initial_value;
}
let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
spec.get_target_value(initial_value, velocity)
}
#[derive(Debug, Clone, Copy)]
pub struct SpringParams {
pub stiffness: f32,
pub damping_ratio: f32,
}
impl SpringParams {
pub const SETTLE_POLICY: Self = Self {
stiffness: 300.0,
damping_ratio: 1.0,
};
pub const OVERSCROLL_BOUNCE: Self = Self {
stiffness: 1909.69,
damping_ratio: 2.71,
};
}
const SETTLE_REST_DISTANCE: f32 = 0.1;
const SETTLE_REST_VELOCITY: f32 = 4.0;
struct SettleAnimationState {
value: Cell<f32>,
velocity: Cell<f32>,
target: f32,
params: SpringParams,
last_frame_time_nanos: Cell<Option<u64>>,
registration: Option<FrameCallbackRegistration>,
is_running: Cell<bool>,
}
pub(crate) struct SettleEnd {
pub(crate) velocity: f32,
pub(crate) hit_boundary: bool,
}
pub struct SettleAnimation {
state: Rc<RefCell<Option<SettleAnimationState>>>,
frame_clock: FrameClock,
params: SpringParams,
}
impl SettleAnimation {
pub fn new(runtime: RuntimeHandle, params: SpringParams) -> Self {
Self {
state: Rc::new(RefCell::new(None)),
frame_clock: runtime.frame_clock(),
params,
}
}
pub(crate) fn start_settle<F, G>(
&self,
initial_value: f32,
initial_velocity: f32,
target: f32,
on_scroll: F,
on_end: G,
) where
F: Fn(f32) -> f32 + 'static,
G: FnOnce(SettleEnd) + 'static,
{
self.cancel();
*self.state.borrow_mut() = Some(SettleAnimationState {
value: Cell::new(initial_value),
velocity: Cell::new(initial_velocity),
target,
params: self.params,
last_frame_time_nanos: Cell::new(None),
registration: None,
is_running: Cell::new(true),
});
schedule_next_settle_frame(
self.state.clone(),
self.frame_clock.clone(),
on_scroll,
on_end,
);
}
pub fn cancel(&self) {
if let Some(state) = self.state.borrow_mut().take() {
state.is_running.set(false);
drop(state.registration);
}
}
pub fn is_running(&self) -> bool {
self.state
.borrow()
.as_ref()
.is_some_and(|s| s.is_running.get())
}
}
impl Clone for SettleAnimation {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
frame_clock: self.frame_clock.clone(),
params: self.params,
}
}
}
fn schedule_next_settle_frame<F, G>(
state: Rc<RefCell<Option<SettleAnimationState>>>,
frame_clock: FrameClock,
on_scroll: F,
on_end: G,
) where
F: Fn(f32) -> f32 + 'static,
G: FnOnce(SettleEnd) + 'static,
{
let state_for_closure = state.clone();
let frame_clock_for_closure = frame_clock.clone();
let on_end = RefCell::new(Some(on_end));
let hit_boundary = Cell::new(false);
let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
let should_continue = {
let state_guard = state_for_closure.borrow();
let Some(anim_state) = state_guard.as_ref() else {
return;
};
if !anim_state.is_running.get() {
return;
}
let dt = match anim_state.last_frame_time_nanos.get() {
Some(last) => (frame_time_nanos.saturating_sub(last) as f32) / 1_000_000_000.0,
None => 0.0,
};
anim_state.last_frame_time_nanos.set(Some(frame_time_nanos));
let (mut next_value, next_velocity) = cranpose_animation::advance_spring(
anim_state.value.get(),
anim_state.velocity.get(),
anim_state.target,
anim_state.params.damping_ratio,
anim_state.params.stiffness,
dt.max(0.0),
);
let is_finished = (next_value - anim_state.target).abs() < SETTLE_REST_DISTANCE
&& next_velocity.abs() < SETTLE_REST_VELOCITY;
if is_finished {
next_value = anim_state.target;
anim_state.is_running.set(false);
}
let delta = next_value - anim_state.value.get();
anim_state.value.set(next_value);
anim_state.velocity.set(next_velocity);
let consumed = if delta.abs() > 0.0001 {
on_scroll(delta)
} else {
delta
};
let boundary_hit = (delta - consumed).abs() > BOUNDARY_EPSILON;
if boundary_hit {
anim_state.is_running.set(false);
hit_boundary.set(true);
}
!is_finished && !boundary_hit
};
if should_continue {
if let Some(on_end_fn) = on_end.borrow_mut().take() {
schedule_next_settle_frame(
state_for_closure.clone(),
frame_clock_for_closure.clone(),
on_scroll,
on_end_fn,
);
}
} else if let Some(end_fn) = on_end.borrow_mut().take() {
let state_guard = state_for_closure.borrow();
let velocity = state_guard
.as_ref()
.map_or(0.0, |anim_state| anim_state.velocity.get());
end_fn(SettleEnd {
velocity,
hit_boundary: hit_boundary.get(),
});
}
});
if let Some(anim_state) = state.borrow_mut().as_mut() {
anim_state.registration = Some(registration);
}
}
#[cfg(test)]
#[path = "tests/fling_animation_tests.rs"]
mod tests;