动画
Ribir 提供了一个强大的动画系统,让您能够创建流畅、交互式的 UI。动画系统建立在 @Animate Widget 和各种过渡机制之上,这些机制使您能够动画化应用程序中的任何状态。
Animate
创建动画的主要方式是通过 @Animate 创建,您能指定动画的持续时间、缓动函数和状态变化。
基本动画
@Animate 需要三个主要属性:
state: 您想要动画化的状态,必须实现AnimateStatetrait, 基本类型的impl StateWriter<T: Clone>已经实现,所以使用中只要获取相应属性的StateWriter即可。from: 动画的起始值transition: 动画应如何随时间推移进行
创建了动画后,只需调用 run 方法即可开始动画的运行。
Ribir 中已经预定义了一些动画 Transition
easing::LINEAR: 以恒定速度动画化easing::EASE_IN: 开始缓慢,加速到结束easing::EASE_OUT: 开始快速,减速到结束easing::EASE_IN_OUT: 开始缓慢,中间加速,然后减速easing::CubicBezierEasing: 三次贝塞尔缓动
下例中,SizedBox 会在首次加载时实现一个跳动的动画。
use ribir::prelude::*;
fn custom_easing_example() -> Widget<'static> {
fn_widget! {
let mut moving_box = @SizedBox {
size: Size::new(50., 50.),
background: Color::RED,
margin: EdgeInsets::horizontal(200.),
};
let animate = @Animate {
state: moving_box.margin(),
from: EdgeInsets::horizontal(0.),
transition: EasingTransition {
duration: Duration::from_millis(1000),
easing: easing::CubicBezierEasing::new(0.68, -0.55, 0.265, 1.55), // 弹跳效果
}.box_it()
};
@SizedBox {
size: Size::new(250., 100.),
@(moving_box) {
on_mounted: move |_| animate.run(),
}
}
}.into_widget()
}
动画生命周期和运行时行为
理解动画生命周期对于有效的动画实现至关重要。Ribir 动画系统遵循动画与状态和渲染管道交互的特定模式:
-
每帧绘制: 在渲染期间,动画修改状态值以反映动画的当前进度。在每一帧中,动画系统根据当前时间和过渡函数计算插值,并用该值临时更新状态。
-
绘制结束: 渲染帧后,动画系统恢复原始状态值。这确保了动画完成后基础数据模型保持不变。
-
状态传播: 在动画期间,状态更改通过
StateWriter的shallow()方法在反应系统中进行修改。此方法更新状态并通知 Widget 系统进行高效重绘,但它不会触发所有监听器(如pipe!块中)的完整反应通知,以避免性能开销和潜在的无限循环。这就是为什么将
Animate直接绑定到 Widget 属性的 Writer(例如widget.map_writer(...))而非独立的Stateful变量至关重要的原因。当绑定到 Widget 属性的 Writer 时,shallow()更新正确通知 Widget 使用新的插值重绘,而其他绑定到该状态的数据则不会收到更新通知。
此行为确保了:
- 动画以高帧率流畅运行
- 动画期间的状态更改不会导致 UI 重排或不必要的重建
- 插值状态值仅在每帧绘制期间被应用;绘制完成后会恢复原始状态,因此这些插值更改不会在动画的绘制步骤之外持续存在
- 通过避免冗余的响应式更新优化动画性能
重要: 将动画绑定到 Widget 的状态(例如使用
map_writer或属性写入器如.opacity())。因为Animate使用shallow()更新,这些更新不会触发联动的更新。
一个常见错误是创建一个独立的 Stateful 变量,动画化它,并使用 pipe! 将其绑定到 Widget。
// ❌ 错误:不要动画化中间状态
fn_widget! {
let opacity_state = Stateful::new(0.0);
let animate = @Animate {
state: opacity_state.clone_writer(),
...
};
@SizedBox {
opacity: pipe!(*$read(opacity_state)),
on_tap: move |_| animate.run(),
}
}
这会失败,因为动画过程中 opacity_state 的值会被修改,但不会扩散,所以 pipe! 不会感知到并触发更新。
正确的做法是直接对 Widget 的状态进行动画的绑定,如下:
// ✅ 正确:动画化 Widget 的状态
fn_widget! {
let w = @SizedBox {
opacity: 1.,
...
};
let animate = @Animate {
state: w.opacity(),
...
};
@(w) { on_tap: move |_| animate.run() }
}
自动绑定动画
Animate 提供了最基础的能力,允许您手动控制动画的开始与停止。此外,Ribir 还提供一种便捷方式,可将动画绑定到属性本身,当属性值通过 StateWriter 更改时自动触发过渡动画。
use ribir::prelude::*;
use ribir::material::md;
fn writer_animate() -> Widget<'static> {
fn_widget! {
let mut w = @Container { size: Size::new(40., 20.) };
w.opacity()
.transition(EasingTransition{
easing: md::easing::STANDARD_ACCELERATE,
duration: md::easing::duration::SHORT2
}.box_it());
let cnt = Stateful::new(0);
@(w) {
on_tap: move |_| {
*$write(cnt) += 1;
if (*$read(cnt) % 2 == 0) {
*$write(w.opacity()) = 1.;
} else {
*$write(w.opacity()) = 0.5;
}
},
background: Color::RED,
}
}.into_widget()
}
这里,w.opacity() 返回的 StateWriter 实现了 AnimateState trait。通过 transition() 方法设置动画属性后,当通过该 StateWriter 修改值时,动画会自动触发。
高级动画
关键帧动画
关键帧允许您在动画中指定中间步骤,提供对复杂动画的细粒度控制。
use ribir::prelude::*;
fn keyframes_example() -> Widget<'static> {
fn_widget! {
let mut box_widget = @SizedBox {
size: Size::new(50., 50.),
background: Color::GREEN,
};
let animate = @Animate {
state: keyframes! {
state: box_widget.map_writer(|w| PartMut::new(&mut w.size)),
0.25 => Size::new(100., 50.), // 在 25% 进度时水平拉伸
0.5 => Size::new(100., 100.), // 在 50% 进度时垂直拉伸
0.75 => Size::new(50., 100.), // 在 75% 进度时水平缩小
1.0 => Size::new(50., 50.), // 在 100% 进度时返回原始
},
from: Size::new(50., 50.),
transition: EasingTransition {
duration: Duration::from_millis(1000),
easing: easing::EASE_IN_OUT,
}.box_it()
};
@(box_widget) {
on_tap: move |_| animate.run(),
}
}.into_widget()
}
关键帧可以使用十进制值(0.0 到 1.0)或百分比定义:
// 使用百分比语法
let keyframe_state = keyframes! {
state: opacity_writer,
20% => 0.2,
50% => 0.5,
80% => 0.8,
};
使用 Stagger 编排动画
为了协调多个动画,Ribir 提供了 Stagger 动画控制器。这允许您创建动画在定时间隔开始的序列,创建复杂的视觉效果:
use ribir::prelude::*;
fn stagger_example() -> Widget<'static> {
fn_widget! {
let stagger = Stagger::new(
Duration::from_millis(200), // 每个动画开始之间 200ms
EasingTransition {
duration: Duration::from_millis(500),
easing: easing::EASE_IN_OUT,
},
);
let mut text1 = @Text { text: "One", opacity: 0. };
let mut text2 = @Text { text: "Two", opacity: 0. };
let mut text3 = @Text { text: "Three", opacity: 0. };
// 向 stagger 添加动画
stagger.write().push_state(text1.opacity(), 0.);
stagger.write().push_state(text2.opacity(), 0.);
stagger.write().push_state(text3.opacity(), 0.);
@Column {
on_mounted: move |_| stagger.run(),
@{ [text1, text2, text3] }
}
}.into_widget()
}
高级 Stagger 功能
Stagger 动画提供额外的控制选项:
- 不同 stagger: 使用
push_animation_with()为每个动画指定不同的时间间隔 - 混合动画: 在同一序列中组合基于状态的动画与完整的
@AnimateWidget - 运行时控制: 使用
is_running()、run_times()和has_ever_run()等方法访问 stagger 状态
use ribir::prelude::*;
fn advanced_stagger_example() -> Widget<'static> {
fn_widget! {
let stagger = Stagger::new(
Duration::from_millis(100),
EasingTransition {
duration: Duration::from_millis(300),
easing: easing::EASE_IN_OUT,
}
);
let mut box1 = @SizedBox { size: Size::new(50., 50.), background: Color::RED, opacity: 0. };
let mut box2 = @SizedBox { size: Size::new(50., 50.), background: Color::GREEN, opacity: 0. };
let mut box3 = @SizedBox { size: Size::new(50., 50.), background: Color::BLUE, opacity: 0. };
// 以不同的 stagger 间隔添加框
stagger.write().push_state(box1.opacity(), 0.);
stagger.write().push_state_with(Duration::from_millis(200), box2.opacity(), 0.); // 等待 200ms
stagger.write().push_animation({
let animate = @Animate {
state: box3.opacity(),
from: 0.,
transition: EasingTransition {
duration: Duration::from_millis(300),
easing: easing::EASE_IN_OUT,
}.box_it()
};
animate
});
@Row {
on_mounted: move |_| stagger.run(),
@{ [box1, box2, box3] }
}
}.into_widget()
}
动画控制
动画可以通过动画实例进行编程控制:
run(): 开始或重新开始动画stop(): 停止动画并恢复状态到最终值is_running(): 检查动画是否正在运行
use ribir::prelude::*;
fn animation_control_example() -> Widget<'static> {
fn_widget! {
let mut box_widget = @SizedBox {
size: Size::new(100., 100.),
background: Color::PURPLE,
opacity: 0.0,
};
let tap_animation = @Animate {
state: box_widget.opacity(),
from: 0.,
transition: EasingTransition {
duration: Duration::from_millis(2000),
easing: easing::EASE_IN_OUT,
}.box_it()
};
let animation = @Animate {
state: box_widget.opacity(),
from: 0., // 开始从当前值动态完成
transition: EasingTransition {
duration: Duration::from_millis(2000),
easing: easing::EASE_IN_OUT,
}.box_it()
};
@Column {
@Row {
@Button {
on_tap: move |_| {
let val = *$read(box_widget.opacity());
*$write(box_widget.opacity()) = 1.0 - val;
// on_tap 处理程序将获取 animation 的所有权,这里使用 $writer 自动克隆
$writer(animation).run();
},
@Text { text: "Start" }
}
@Button {
on_tap: move |_| {
animation.stop();
},
@Text { text: "Stop" }
}
}
@ { box_widget }
}
}.into_widget()
}
动画组合
动画可以组合和分层以创建复杂效果。您可以:
- 并行运行多个动画
- 单次动画同时修改多个属性
use ribir::prelude::*;
fn composition_example() -> Widget<'static> {
fn_widget! {
let mut box_widget = @SizedBox {
size: Size::new(50., 50.),
background: Color::BLUE,
opacity: 0.,
transform: Transform::identity(),
};
let opacity_size_anim = @Animate {
state: (box_widget.opacity(), box_widget.map_writer(|w| PartMut::new(&mut w.size))),
from: (0., Size::new(50., 50.)),
transition: EasingTransition {
duration: Duration::from_millis(1000),
easing: easing::EASE_IN_OUT,
}.box_it()
};
let rotation_anim = @Animate {
state: box_widget.transform(),
from: Transform::identity(),
transition: EasingTransition {
duration: Duration::from_millis(2000),
easing: easing::LINEAR,
}.box_it()
};
@(box_widget) {
on_tap: move |_| {
opacity_size_anim.run();
rotation_anim.run();
},
}
}.into_widget()
}
高级过渡修饰符
动画可以使用各种过渡修饰符增强以提供额外功能。两个常见的修饰符是 repeat 和 delay。
重复与延迟动画
动画可以同时使用 repeat 和 delay 过渡修饰符。下面的示例展示了一个动画:它在开始前等待 1000ms,然后重复三次,通过将不透明度从 0 动画到 1 来实现闪烁效果。
use ribir::prelude::*;
fn transition_modifiers_example() -> Widget<'static> {
fn_widget! {
let mut box_widget = @SizedBox {
size: Size::new(200., 100.),
background: Color::YELLOW,
opacity: 1.,
};
let animate = @Animate {
state: box_widget.opacity(),
from: 0.,
transition: EasingTransition {
duration: Duration::from_millis(100),
easing: easing::steps(2, easing::StepsJump::JumpNone),
}
.repeat(3.) // 重复3次
.delay(Duration::from_millis(1000)) // 延迟1000ms 再执行
.box_it()
};
@(box_widget) {
on_mounted: move |_| animate.run(), // Start the animation after delay with repetitions
}
}.into_widget()
}
动画是创建引人入胜、直观的用户体验的强大工具。通过掌握 Ribir 中的动画系统,您可以创建流畅、响应式的应用程序,使其感觉生动和交互式。