第十六章:动画与过渡效果
博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)
第十六章:动画与过渡效果
1. Dioxus 动画方案概览
| 方案 | 实现方式 | 适用场景 | 性能 |
|------|---------|---------|------|
| CSS Transition | transition-* 类 | 悬停、显隐、简单状态变化 | 极高(GPU 加速) |
| CSS Animation | @keyframes | 加载提示、强调效果、循环动画 | 极高 |
| 条件类名切换 | Signal + 类名拼接 | 进入/离开、展开/收起 | 高 |
| JS 动画 | requestAnimationFrame | 复杂路径、物理模拟 | 中 |
| Web API | web-sys 绑定 | Canvas、WebGL | 低(按需) |
2. CSS Transition 基础
2.1 属性过渡
button {
class: "transition-all duration-200 ease-in-out",
style: "
background: {if hovered() { \"var(--primary)\" } else { \"var(--card)\" }};
transform: {if hovered() { \"scale(1.02)\" } else { \"scale(1)\" }};
box-shadow: {if hovered() { \"0 4px 12px rgba(0,0,0,0.1)\" } else { \"0 1px 3px rgba(0,0,0,0.05)\" }};
",
}
// 使用 Tailwind 工具类更简洁
button {
class: "
px-4 py-2 rounded-lg
transition-all duration-200
hover:bg-blue-500 hover:text-white hover:shadow-lg
active:scale-95
",
}
2.2 控制过渡触发时机
#[component]
fn ExpandableCard(title: String, children: Element) -> Element {
let expanded = use_signal(|| false);
rsx! {
div { class: "border rounded-lg overflow-hidden",
// 点击标题展开/收起
button {
class: "w-full px-4 py-3 flex items-center justify-between
transition-colors duration-150 hover:bg-gray-50",
onclick: move |_| expanded.toggle(),
span { "{title}" }
span {
class: "transition-transform duration-200",
style: "transform: rotate({if expanded() { \"180deg\" } else { \"0deg\" }});",
"▼"
}
}
// 内容区域:高度过渡
div {
class: "transition-all duration-300 ease-in-out overflow-hidden",
style: "
max-height: {if expanded() { \"500px\" } else { \"0\" }};
opacity: {if expanded() { \"1\" } else { \"0\" }};
",
div { class: "px-4 py-3", {children} }
}
}
}
}
2.3 多步骤过渡
#[component]
fn MultiStepTransition() -> Element {
let state = use_signal(|| 0u8);
// 使用 use_memo 计算每个阶段的样式
let card_style = use_memo(move || {
match state() {
0 => "opacity:0; transform: translateY(20px) scale(0.95);".to_string(),
1 => "opacity:1; transform: translateY(0) scale(1);".to_string(),
2 => "opacity:0; transform: translateY(-20px) scale(1.05);".to_string(),
_ => "opacity:0;".to_string(),
}
});
rsx! {
div {
div {
class: "transition-all duration-500 ease-out",
style: "{card_style}",
"内容卡片"
}
button {
onclick: move |_| {
if state() < 2 { state += 1; } else { state.set(0); }
},
"下一步"
}
}
}
}
3. CSS Animation 与 @keyframes
3.1 加载骨架屏
#[component]
fn SkeletonCard() -> Element {
rsx! {
div { class: "border rounded-lg p-4 space-y-3",
div { class: "h-4 bg-gray-200 rounded animate-pulse w-3/4" }
div { class: "h-4 bg-gray-200 rounded animate-pulse" }
div { class: "h-4 bg-gray-200 rounded animate-pulse w-5/6" }
div { class: "h-20 bg-gray-200 rounded animate-pulse" }
}
}
}
3.2 自定义动画
/* main.css */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideInRight {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
@keyframes pulse-dot {
0%, 80%, 100% { transform: scale(0); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
3.3 使用自定义动画
// 列表项逐个入场
#[component]
fn AnimatedList(items: Vec<ListItem>) -> Element {
rsx! {
for (i, item) in items.iter().enumerate() {
div {
key: "{item.id}",
class: "transition-all duration-300",
style: "
animation: fadeInUp 0.4s ease-out {i * 80}ms both;
",
// 每个项目延迟 80ms 依次出现
ListItemCard { item: item.clone() }
}
}
}
}
// 骨架屏闪烁效果
div {
class: "rounded-lg",
style: "
background: linear-gradient(90deg,
var(--hover) 25%,
var(--card) 50%,
var(--hover) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
height: 200px;
",
}
4. 条件渲染动画
key id="4-1-使用-触发进入离开动画">4.1 使用 触发进入/离开动画
// 当 key 变化时,Dioxus 会移除旧元素并创建新元素
// 结合 CSS animation 实现切换效果
div {
key: "{current_view}",
class: "animate-fadeIn",
match current_view() {
"list" => rsx! { ArticleList {} },
"detail" => rsx! { ArticleDetail {} },
_ => rsx! { div {} },
}
}
4.2 if 条件动画
#[component]
fn Toast(message: Signal<Option<String>>) -> Element {
let visible = message().is_some();
rsx! {
div {
class: "
fixed top-4 right-4 z-50 px-4 py-3 rounded-lg shadow-lg
transition-all duration-300
text-sm text-white
",
style: "
background: var(--primary);
opacity: {if visible { \"1\" } else { \"0\" }};
transform: translateY({if visible { \"0\" } else { \"-20px\" }});
pointer-events: {if visible { \"auto\" } else { \"none\" }};
",
if let Some(msg) = &*message.read() {
span { "{msg}" }
}
}
}
}
5. 交互动画
5.1 按钮涟漪效果
#[component]
fn RippleButton(children: Element) -> Element {
let mut ripples = use_signal(|| Vec::<(f64, f64, u64)>::new());
rsx! {
button {
class: "relative overflow-hidden px-6 py-2 rounded-lg
bg-blue-500 text-white font-medium",
onclick: move |e| {
let rect = e.get_client_rect();
let x = e.client_x() - rect.left;
let y = e.client_y() - rect.top;
let id = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap().as_millis() as u64;
ripples.write().push((x, y, id));
// 动画结束后自动移除
let mut r = ripples;
spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(600)).await;
r.write().retain(|&(_, _, i)| i != id);
});
},
{children}
for (x, y, _) in ripples.read().iter() {
div {
class: "absolute w-4 h-4 rounded-full bg-white/30 animate-ripple",
style: "left: {x - 8}px; top: {y - 8}px;",
}
}
}
}
}
CSS:
@keyframes ripple {
from { transform: scale(0); opacity: 0.5; }
to { transform: scale(15); opacity: 0; }
}
.animate-ripple { animation: ripple 0.6s ease-out both; }
5.2 滚动触发动画
#[component]
fn ScrollReveal(children: Element) -> Element {
let visible = use_signal(|| false);
let element_ref = use_node_ref();
use_effect(move || {
// Intersection Observer 检测元素进入视口
let el = element_ref.get();
if let Some(element) = el {
// 通过 web-sys 创建 IntersectionObserver
// 元素进入视口时设置 visible = true
}
});
rsx! {
div {
class: "transition-all duration-700",
style: "
opacity: {if visible() { \"1\" } else { \"0\" }};
transform: translateY({if visible() { \"0\" } else { \"40px\" }});
",
onmounted: move |_| visible.set(true), // 简化:直接挂载时显示
{children}
}
}
}
6. 路由过渡动画
use dioxus::prelude::*;
#[component]
fn AnimatedLayout() -> Element {
// 通过 key 触发组件级别的过渡
let route_key = use_context::<Signal<String>>();
rsx! {
div {
class: "transition-all duration-300",
key: "{route_key}",
style: "
animation: fadeInUp 0.3s ease-out;
",
Outlet::<Route> {}
}
}
}
7. 列表项拖拽动画
#[component]
fn DraggableList() -> Element {
let items = use_signal(|| vec!["A", "B", "C", "D", "E"]);
// 简单实现:点击上下移动
let move_up = move |index: usize| {
if index == 0 { return; }
items.write().swap(index, index - 1);
};
let move_down = move |index: usize| {
if index >= items.read().len() - 1 { return; }
items.write().swap(index, index + 1);
};
rsx! {
div { class: "space-y-2",
for (i, item) in items.iter().enumerate() {
div {
key: "{item}",
class: "flex items-center gap-2 px-4 py-3 rounded-lg
transition-all duration-300 border",
style: "background: var(--card); border-color: var(--border);",
span { class: "flex-1", "{item}" }
button { onclick: move |_| move_up(i), "↑" }
button { onclick: move |_| move_down(i), "↓" }
}
}
}
}
}
8. 性能优化建议
| 优化点 | 说明 |
|--------|------|
| 优先使用 transform 和 opacity | 这两个属性由 GPU 合成,不触发重排 |
| 避免频繁读写 layout 属性 | width/height/top/left 会触发重排 |
| 使用 will-change 提示浏览器 | will-change: transform, opacity; |
| 动画元素提升为独立图层 | transform: translateZ(0); 或 will-change: transform; |
| requestAnimationFrame 控制 JS 动画 | 与浏览器刷新率同步,避免掉帧 |
| 动画持续时间控制在 200-500ms | 太短看不清,太长显拖沓 |
// 高性能动画的 CSS 写法
div {
class: "transition-[transform,opacity] duration-300",
style: "
will-change: transform, opacity;
transform: translateZ(0); /* 启用 GPU 加速 */
",
}
9. 小结
- CSS Transition 适合简单状态变化,CSS Animation 适合复杂/循环动画
- 条件渲染时用
key触发元素的创建/销毁动画 use_memo预计算动态样式,避免在渲染中重复计算- 动画优先使用
transform和opacity,性能最佳 - 骨架屏和微交互动画提升用户体验
- 列表项错开延迟依次入场,效果比整体出现好得多
dioxusanimationtransitioncssux