RustSlint实现动态启动界面源码分享一、效果展示二、源码分享1、工程结构2、main.rs3、build.rs4、main.slint5、Cargo.toml三、实现原理1、整体数据流2、动画机制2.1 发射动画2.2 旋转动画2.3 淡出效果3、资源管理3.1 图片资源3.2 模型增删4、关键设计要点一、效果展示二、源码分享1、工程结构2、main.rsslint::include_modules!();useslint::{ModelRc,Timer,TimerMode,VecModel};usestd::rc::Rc;usestd::time::{Duration,Instant};/// 简易伪随机数生成器LCG返回 0.0 ~ 1.0fnpseudo_random(seed:i32)-f32{letx((seedasi64).wrapping_mul(1_103_515_245).wrapping_add(12_345))0x7fff_ffff;xasf32/0x7fff_ffffasf32}fnmain(){letuiMainWindow::new().expect(无法创建主窗口);// ── 浮动图片模型 ──letmodel:RcVecModelFloatingImageRc::new(VecModel::from(vec![]));ui.set_floating_images(ModelRc::new(model.clone()));// ── 内部状态每张图片的创建时间用于清理 ──letcreated_times:Rcstd::cell::RefCellVecInstantRc::new(std::cell::RefCell::new(Vec::new()));letcounterRc::new(std::cell::Cell::new(0_i32));// ── 注册 spawn-image 回调Slint Timer 每 2 秒调用一次 ──{letmodelmodel.clone();letcreated_timescreated_times.clone();letcountercounter.clone();ui.on_spawn_image(move||{letidcounter.get();counter.set(id1);letstop_x_ratiopseudo_random(id);model.push(FloatingImage{id,stop_x_ratio});created_times.borrow_mut().push(Instant::now());});}// ── 清理定时器每秒清理一次超过 15.5 秒的图片 ──{letmodelmodel.clone();letcreated_timescreated_times.clone();letcleanup_timerTimer::default();cleanup_timer.start(TimerMode::Repeated,Duration::from_secs(1),move||{letnowInstant::now();letmuttimescreated_times.borrow_mut();// 找出过期的索引从高到低避免 remove 时索引偏移// 动画总时长 8 秒留 500ms 缓冲letmutexpired:Vecusizetimes.iter().enumerate().filter(|(_,t)|now.duration_since(**t)Duration::from_millis(8_500)).map(|(i,_)|i).collect();expired.sort_unstable_by(|a,b|b.cmp(a));// 逐个移除不重建整个模型保留其他元素的动画状态foridxinexpired{times.remove(idx);model.remove(idx);}},);std::mem::forget(cleanup_timer);}ui.run().expect(运行 UI 失败);}3、build.rsfnmain(){slint_build::compile(ui/main.slint).unwrap()}4、main.slint// 动态背景 // 浮动图片数据结构exportstructFloatingImage{id:int,// 目标位置占屏幕宽度的比例 (0.0 ~ 1.0)由 Rust 随机生成stop-x-ratio:float,}// 图片资源数组在 Slint 中加载globalImageResources{inproperty[image]images:[image-url(../images/apple.svg),image-url(../images/banana.svg),image-url(../images/watermelon.svg),image-url(../images/peach.svg),image-url(../images/mango.svg),image-url(../images/strawberry.svg),];}export componentMainWindowinheritsWindow{title:动态背景;preferred-width:800px;preferred-height:600px;// 浮动图片模型由 Rust 端管理增删inproperty[FloatingImage]floating-images;// 回调请求 Rust 添加新图片到模型callback spawn-image();// 渐变背景与 QML 一致从上到下background:linear-gradient(180deg,#4bb6ac0%,#3edabd50%,#24efc0100%);// 生成定时器 // 每 2 秒触发一次调用 Rust 回调添加新图片Timer{interval:2000ms;running:true;triggered(){spawn-image();}}// 浮动图片层 forimginfloating-images:Rectangle{width:100px;height:100px;// 动画状态是否已发射propertyboollaunched:false;// 旋转角度0 ~ 360 循环propertyfloatrotation-angle:0;// 起始位置float 计算最终转 lengthpropertyfloatstart-x-px:root.width/1px/2-50;propertyfloatstart-y-px:root.height/1px;// 终止位置propertyfloatstop-x-px:(root.width/1px-100)*img.stop-x-ratio;propertyfloatstop-y-px:-150;// 当前位置根据 launched 状态决定x:(launched?stop-x-px:start-x-px)*1px;y:(launched?stop-y-px:start-y-px)*1px;// 旋转使用 transform-rotationtransform-rotation:rotation-angle*1deg;// 动画x 和 y 变化时使用 animate加速到 8 秒animate x,y{duration:8000ms;easing:linear;}// 透明度动画整个 8 秒内逐渐淡出opacity:launched?0.0:1.0;animate opacity{duration:8000ms;easing:linear;}// 发射定时器创建后 10ms 触发将 launched 设为 true 以启动动画Timer{interval:10ms;running:!launched;triggered(){launchedtrue;}}// 旋转定时器每 50ms 增加 6 度3 秒一圈Timer{interval:50ms;running:true;triggered(){rotation-angleMath.mod(rotation-angle6,360);}}// 图片渲染从全局资源数组中按 id 取模获取Image{width:100px;height:100px;source:ImageResources.images[Math.mod(img.id,6)];}}// 清理逻辑由 Rust 端处理每秒检查并移除超过 15.5 秒的图片// 中央文字 Text{x:(root.width-self.width)/2;y:(root.height-self.height)/2;text:正在启动中...请稍后;color:white;font-size:30px;font-weight:700;horizontal-alignment:center;vertical-alignment:center;}}5、Cargo.toml[package]namemodbus-debug-assistantversion0.1.0edition2024[dependencies]slint1.17.1[build-dependencies]slint-build1.17.1三、实现原理本项目的核心思路是Rust 负责数据与逻辑、Slint 负责界面与动画两者通过 Slint 提供的回调callback与模型model机制进行通信。下面从数据流、动画机制和资源管理三个维度拆解实现细节。1、整体数据流整个动态启动界面的运行流程如下Slint Timer 每 2 秒触发调用 spawn-image 回调Rust 生成随机目标位置向 VecModel 推送 FloatingImageSlint for 循环渲染新图片图片发射动画 旋转 淡出Rust 清理定时器移除过期图片Slint 侧Timer组件每 2 秒触发一次spawn-image()回调通知 Rust 端生成新图片。Rust 侧收到回调后用 LCG 伪随机数生成器计算目标横坐标比例构造FloatingImage结构体并推入VecModel。Slint 侧for img in floating-images响应模型变化自动为每条新数据创建对应的Rectangle组件实例并启动动画。2、动画机制2.1 发射动画每张图片创建后其launched属性初始为false此时图片停留在屏幕底部起始位置。图片内部的Timer在 10ms 后触发将launched置为true从而触发x、y属性的变化x: (launched ? stop-x-px : start-x-px) * 1px; y: (launched ? stop-y-px : start-y-px) * 1px; animate x, y { duration: 8000ms; easing: linear; }由于animate声明了 8 秒的线性过渡Slint 会自动在起始位置与终止位置之间插值形成图片从底部缓缓飘向顶部的效果。2.2 旋转动画旋转通过独立的Timer实现每 50ms 将rotation-angle增加 6 度配合transform-rotation属性让图片持续旋转Timer { interval: 50ms; running: true; triggered() { rotation-angle Math.mod(rotation-angle 6, 360); } }2.3 淡出效果透明度动画与位移动画同步进行同样持续 8 秒从完全不透明渐变为完全透明opacity: launched ? 0.0 : 1.0; animate opacity { duration: 8000ms; easing: linear; }3、资源管理3.1 图片资源六张水果图片通过global ImageResources统一管理在 Slint 中直接加载 SVG 资源。渲染时按图片id对 6 取模实现循环使用不同图片source: ImageResources.images[Math.mod(img.id, 6)];3.2 模型增删Rust 端维护VecModelFloatingImage作为数据源同时用VecInstant记录每张图片的创建时间。清理定时器每秒检查一次将存在超过 8.5 秒的图片索引从高到低逐个移除letmutexpired:Vecusizetimes.iter().enumerate().filter(|(_,t)|now.duration_since(**t)Duration::from_millis(8_500)).map(|(i,_)|i).collect();expired.sort_unstable_by(|a,b|b.cmp(a));foridxinexpired{times.remove(idx);model.remove(idx);}这里有两个关键设计从高到低移除避免remove时索引偏移导致误删。不重建整个模型只移除过期元素保留其他图片的动画状态避免界面闪烁。4、关键设计要点设计点说明数据与界面分离Rust 只负责数据增删与随机逻辑Slint 只负责渲染与动画回调驱动Slint 通过callback主动请求数据而非 Rust 轮询界面模型响应式VecModel的增删会自动同步到界面无需手动刷新动画状态独立每张图片的launched、rotation-angle均为独立属性互不干扰定时器分工生成定时器在 Slint 侧清理定时器在 Rust 侧各司其职