Skip to main content

cranpose_ui/widgets/
animated_visibility.rs

1//! AnimatedVisibility composable
2//!
3//! Mirrors Jetpack Compose's `AnimatedVisibility` from
4//! `androidx.compose.animation.AnimatedVisibility`, with a minimal set of
5//! enter/exit transitions: `fade_in`/`fade_out` and
6//! `slide_in_vertically`/`slide_out_vertically`.
7
8use cranpose_animation::AnimationType;
9
10use crate::{
11    composable,
12    modifier::{GraphicsLayer, Modifier},
13    widgets::{
14        box_widget::{Box, BoxSpec},
15        crossfade::animate_float_with_initial,
16    },
17};
18
19/// Progress below which exiting content is considered fully hidden and can
20/// leave the composition.
21const VISIBILITY_PROGRESS_EPSILON: f32 = 0.001;
22
23/// Defines how an [`AnimatedVisibility`] content appears.
24///
25/// Mirrors Jetpack Compose's `EnterTransition`. Transitions are combined
26/// with `+`, like Compose's `fadeIn() + slideInVertically()`.
27#[derive(Clone, Copy, Debug, PartialEq)]
28pub struct EnterTransition {
29    fade: bool,
30    slide_vertical_fraction: Option<f32>,
31    animation: Option<AnimationType>,
32}
33
34impl EnterTransition {
35    /// An empty enter transition: content appears without any effect.
36    ///
37    /// Mirrors Jetpack Compose: `EnterTransition.None`.
38    pub fn none() -> Self {
39        Self {
40            fade: false,
41            slide_vertical_fraction: None,
42            animation: None,
43        }
44    }
45
46    /// Use a custom animation spec for this transition. In Compose the spec
47    /// is a parameter of each transition factory (e.g.
48    /// `fadeIn(animationSpec = tween(300))`); here it is a builder method:
49    /// `fade_in().with_animation(tween(300, Easing::LinearEasing))`.
50    pub fn with_animation(mut self, animation: AnimationType) -> Self {
51        self.animation = Some(animation);
52        self
53    }
54
55    pub(crate) fn has_fade(&self) -> bool {
56        self.fade
57    }
58
59    pub(crate) fn slide_vertical_fraction(&self) -> Option<f32> {
60        self.slide_vertical_fraction
61    }
62
63    pub(crate) fn animation(&self) -> AnimationType {
64        self.animation.unwrap_or_default()
65    }
66}
67
68impl std::ops::Add for EnterTransition {
69    type Output = Self;
70
71    fn add(self, other: Self) -> Self {
72        Self {
73            fade: self.fade || other.fade,
74            slide_vertical_fraction: self
75                .slide_vertical_fraction
76                .or(other.slide_vertical_fraction),
77            animation: self.animation.or(other.animation),
78        }
79    }
80}
81
82/// Defines how an [`AnimatedVisibility`] content disappears.
83///
84/// Mirrors Jetpack Compose's `ExitTransition`. Transitions are combined
85/// with `+`, like Compose's `fadeOut() + slideOutVertically()`.
86#[derive(Clone, Copy, Debug, PartialEq)]
87pub struct ExitTransition {
88    fade: bool,
89    slide_vertical_fraction: Option<f32>,
90    animation: Option<AnimationType>,
91}
92
93impl ExitTransition {
94    /// An empty exit transition: content disappears without any effect.
95    ///
96    /// Mirrors Jetpack Compose: `ExitTransition.None`.
97    pub fn none() -> Self {
98        Self {
99            fade: false,
100            slide_vertical_fraction: None,
101            animation: None,
102        }
103    }
104
105    /// Use a custom animation spec for this transition (see
106    /// [`EnterTransition::with_animation`]).
107    pub fn with_animation(mut self, animation: AnimationType) -> Self {
108        self.animation = Some(animation);
109        self
110    }
111
112    pub(crate) fn has_fade(&self) -> bool {
113        self.fade
114    }
115
116    pub(crate) fn slide_vertical_fraction(&self) -> Option<f32> {
117        self.slide_vertical_fraction
118    }
119
120    pub(crate) fn animation(&self) -> AnimationType {
121        self.animation.unwrap_or_default()
122    }
123}
124
125impl std::ops::Add for ExitTransition {
126    type Output = Self;
127
128    fn add(self, other: Self) -> Self {
129        Self {
130            fade: self.fade || other.fade,
131            slide_vertical_fraction: self
132                .slide_vertical_fraction
133                .or(other.slide_vertical_fraction),
134            animation: self.animation.or(other.animation),
135        }
136    }
137}
138
139/// Fades in the content of an [`AnimatedVisibility`], from transparent to
140/// fully opaque.
141///
142/// Mirrors Jetpack Compose: `fadeIn()`.
143pub fn fade_in() -> EnterTransition {
144    EnterTransition {
145        fade: true,
146        ..EnterTransition::none()
147    }
148}
149
150/// Fades out the content of an [`AnimatedVisibility`], from fully opaque to
151/// transparent.
152///
153/// Mirrors Jetpack Compose: `fadeOut()`.
154pub fn fade_out() -> ExitTransition {
155    ExitTransition {
156        fade: true,
157        ..ExitTransition::none()
158    }
159}
160
161/// Slides in the content vertically from `initial_offset_fraction` of its
162/// own height (negative values start above the resting position, mirroring
163/// Compose's default of `-fullHeight / 2`).
164///
165/// Mirrors Jetpack Compose: `slideInVertically { fullHeight -> ... }`, with
166/// the offset expressed as a fraction of the content height instead of a
167/// lambda over the measured height.
168pub fn slide_in_vertically(initial_offset_fraction: f32) -> EnterTransition {
169    EnterTransition {
170        slide_vertical_fraction: Some(initial_offset_fraction),
171        ..EnterTransition::none()
172    }
173}
174
175/// Slides out the content vertically towards `target_offset_fraction` of
176/// its own height.
177///
178/// Mirrors Jetpack Compose: `slideOutVertically { fullHeight -> ... }`, with
179/// the offset expressed as a fraction of the content height instead of a
180/// lambda over the measured height.
181pub fn slide_out_vertically(target_offset_fraction: f32) -> ExitTransition {
182    ExitTransition {
183        slide_vertical_fraction: Some(target_offset_fraction),
184        ..ExitTransition::none()
185    }
186}
187
188/// Animates the appearance and disappearance of its content when `visible`
189/// changes.
190///
191/// Mirrors Jetpack Compose:
192/// `AnimatedVisibility(visible, enter = ..., exit = ...) { ... }`.
193///
194/// Compose semantics: content that is visible on first composition appears
195/// without an animation; when `visible` turns `false` the content stays
196/// composed for the whole exit transition and leaves the composition once it
197/// completes; toggling `visible` mid-transition retargets the running
198/// animation from its current value.
199///
200/// Divergence from Compose: enter and exit each drive a single shared
201/// progress (instead of one `Transition` animation per effect), so all
202/// effects of a combined transition share one animation spec. Exiting with
203/// `ExitTransition::none()` still holds the content for the duration of the
204/// exit spec before removal.
205#[composable]
206pub fn AnimatedVisibility<F>(
207    visible: bool,
208    enter: EnterTransition,
209    exit: ExitTransition,
210    content: F,
211) where
212    F: FnMut() + 'static,
213{
214    let target = if visible { 1.0 } else { 0.0 };
215    let animation = if visible {
216        enter.animation()
217    } else {
218        exit.animation()
219    };
220    let progress_state = animate_float_with_initial(target, target, animation);
221    let progress = progress_state.value();
222
223    let composed = visible || progress > VISIBILITY_PROGRESS_EPSILON;
224    if composed {
225        let fade = if visible {
226            enter.has_fade()
227        } else {
228            exit.has_fade()
229        };
230        let slide_fraction = if visible {
231            enter.slide_vertical_fraction()
232        } else {
233            exit.slide_vertical_fraction()
234        };
235
236        let alpha = if fade { progress.clamp(0.0, 1.0) } else { 1.0 };
237        let mut modifier = Modifier::empty().graphics_layer_value(GraphicsLayer {
238            alpha,
239            ..Default::default()
240        });
241        if let Some(fraction) = slide_fraction {
242            modifier = modifier.offset_fraction(0.0, fraction * (1.0 - progress));
243        }
244
245        Box(modifier, BoxSpec::new(), content);
246    }
247}