Rust+Slint 实现动态加载效果源码分享

Rust+Slint 实现抽屉式侧边栏源码分享

一、效果展示

二、源码分享

1、工程结构

2、main.rs

rust 复制代码
slint::include_modules!();

fn main() -> Result<(), slint::PlatformError> {
    let main_window = MainWindow::new()?;
    main_window.run()
}

3、build.rs

rust 复制代码
fn main() {
    // 编译 Slint UI
    slint_build::compile("ui/main.slint").unwrap();
}

4、main.slint

rust 复制代码
// 发光点(带光晕)
component GlowDot inherits Rectangle {
    in property <color> glow-color: #4fc3f7;
    in property <length> dot-size: 8px;
    in property <float> glow-opacity: 0.3;

    width: dot-size * 2.5;
    height: dot-size * 2.5;
    border-radius: self.width / 2;
    background: glow-color;
    opacity: glow-opacity;
}

// 普通加载点
component LoadingDot inherits Rectangle {
    in property <color> dot-color: #4fc3f7;
    in property <length> dot-size: 8px;

    width: dot-size;
    height: dot-size;
    border-radius: dot-size / 2;
    background: dot-color;
}

//  sparkle 闪烁点
component Sparkle inherits Rectangle {
    in property <color> sparkle-color: #ffffff;
    in property <length> sparkle-size: 3px;
    in property <float> sparkle-opacity: 0.5;

    width: sparkle-size;
    height: sparkle-size;
    border-radius: sparkle-size / 2;
    background: sparkle-color;
    opacity: sparkle-opacity;
}

// 主环形加载组件
export component RingLoading inherits Rectangle {
    in property <color> primary-color: #4fc3f7;
    in property <color> secondary-color: #ff4081;
    in property <color> track-color: #2a3a4a;
    in property <length> ring-size: 120px;
    in property <length> dot-size: 10px;
    in property <bool> running: true;
    in property <string> label: "Loading...";

    width: ring-size + dot-size * 4 + 30px;
    height: ring-size + dot-size * 4 + 50px;
    background: transparent;

    // 旋转角度
    property <angle> outer-angle: 0deg;
    property <angle> inner-angle: 0deg;
    property <angle> pulse-phase: 0deg;

    // 中心点
    property <length> cx: self.width / 2;
    property <length> cy: (self.height - 36px) / 2;

    // 外环半径 & 内环半径
    property <length> outer-radius: ring-size / 2;
    property <length> inner-radius: ring-size / 3.2;

    // 尾巴间距
    property <angle> outer-tail-spacing: 16deg;
    property <angle> inner-tail-spacing: 20deg;

    // 脉冲值 (0~1)
    property <float> pulse: (1 + Math.sin(pulse-phase)) / 2;
    // 呼吸值 (用于轨道)
    property <float> breathe: 0.3 + pulse * 0.4;

    // ========== 外环轨道(呼吸效果) ==========
    Rectangle {
        x: cx - ring-size / 2;
        y: cy - ring-size / 2;
        width: ring-size;
        height: ring-size;
        border-radius: ring-size / 2;
        border-width: dot-size * 0.5;
        border-color: track-color;
        background: transparent;
        opacity: breathe;
    }

    // ========== 内环轨道(呼吸效果,反相) ==========
    property <float> inner-breathe: 0.7 - pulse * 0.4;
    Rectangle {
        x: cx - inner-radius * 2;
        y: cy - inner-radius * 2;
        width: inner-radius * 4;
        height: inner-radius * 4;
        border-radius: inner-radius * 2;
        border-width: dot-size * 0.4;
        border-color: track-color;
        background: transparent;
        opacity: inner-breathe;
    }

    // ========== 外环彗星尾(顺时针,蓝色系渐变) ==========
    // 光晕(在头部点后面)
    GlowDot {
        glow-color: primary-color;
        dot-size: dot-size;
        x: cx + outer-radius * Math.cos(outer-angle) - dot-size * 2.5 / 2;
        y: cy + outer-radius * Math.sin(outer-angle) - dot-size * 2.5 / 2;
        glow-opacity: 0.25 + pulse * 0.15;
    }
    // 头部点
    LoadingDot {
        dot-color: primary-color;
        dot-size: dot-size;
        x: cx + outer-radius * Math.cos(outer-angle) - dot-size / 2;
        y: cy + outer-radius * Math.sin(outer-angle) - dot-size / 2;
        opacity: 1.0;
    }
    // 尾迹点 1
    LoadingDot {
        dot-color: #49b8f0;
        dot-size: dot-size * 0.94;
        x: cx + outer-radius * Math.cos(outer-angle - outer-tail-spacing) - dot-size * 0.94 / 2;
        y: cy + outer-radius * Math.sin(outer-angle - outer-tail-spacing) - dot-size * 0.94 / 2;
        opacity: 0.88;
    }
    // 尾迹点 2
    LoadingDot {
        dot-color: #44adf3;
        dot-size: dot-size * 0.88;
        x: cx + outer-radius * Math.cos(outer-angle - outer-tail-spacing * 2) - dot-size * 0.88 / 2;
        y: cy + outer-radius * Math.sin(outer-angle - outer-tail-spacing * 2) - dot-size * 0.88 / 2;
        opacity: 0.74;
    }
    // 尾迹点 3
    LoadingDot {
        dot-color: #4499f5;
        dot-size: dot-size * 0.82;
        x: cx + outer-radius * Math.cos(outer-angle - outer-tail-spacing * 3) - dot-size * 0.82 / 2;
        y: cy + outer-radius * Math.sin(outer-angle - outer-tail-spacing * 3) - dot-size * 0.82 / 2;
        opacity: 0.58;
    }
    // 尾迹点 4
    LoadingDot {
        dot-color: #4c82f5;
        dot-size: dot-size * 0.76;
        x: cx + outer-radius * Math.cos(outer-angle - outer-tail-spacing * 4) - dot-size * 0.76 / 2;
        y: cy + outer-radius * Math.sin(outer-angle - outer-tail-spacing * 4) - dot-size * 0.76 / 2;
        opacity: 0.43;
    }
    // 尾迹点 5
    LoadingDot {
        dot-color: #5968f2;
        dot-size: dot-size * 0.7;
        x: cx + outer-radius * Math.cos(outer-angle - outer-tail-spacing * 5) - dot-size * 0.7 / 2;
        y: cy + outer-radius * Math.sin(outer-angle - outer-tail-spacing * 5) - dot-size * 0.7 / 2;
        opacity: 0.3;
    }
    // 尾迹点 6
    LoadingDot {
        dot-color: #6a50ea;
        dot-size: dot-size * 0.64;
        x: cx + outer-radius * Math.cos(outer-angle - outer-tail-spacing * 6) - dot-size * 0.64 / 2;
        y: cy + outer-radius * Math.sin(outer-angle - outer-tail-spacing * 6) - dot-size * 0.64 / 2;
        opacity: 0.18;
    }
    // 尾迹点 7
    LoadingDot {
        dot-color: #7c3be0;
        dot-size: dot-size * 0.58;
        x: cx + outer-radius * Math.cos(outer-angle - outer-tail-spacing * 7) - dot-size * 0.58 / 2;
        y: cy + outer-radius * Math.sin(outer-angle - outer-tail-spacing * 7) - dot-size * 0.58 / 2;
        opacity: 0.09;
    }
    // 尾迹点 8
    LoadingDot {
        dot-color: #8e2ad6;
        dot-size: dot-size * 0.52;
        x: cx + outer-radius * Math.cos(outer-angle - outer-tail-spacing * 8) - dot-size * 0.52 / 2;
        y: cy + outer-radius * Math.sin(outer-angle - outer-tail-spacing * 8) - dot-size * 0.52 / 2;
        opacity: 0.04;
    }

    // ========== 内环彗星尾(逆时针,粉红系渐变) ==========
    // 光晕
    GlowDot {
        glow-color: secondary-color;
        dot-size: dot-size * 0.75;
        x: cx + inner-radius * Math.cos(inner-angle) - dot-size * 0.75 * 2.5 / 2;
        y: cy + inner-radius * Math.sin(inner-angle) - dot-size * 0.75 * 2.5 / 2;
        glow-opacity: 0.2 + pulse * 0.15;
    }
    // 头部点
    LoadingDot {
        dot-color: secondary-color;
        dot-size: dot-size * 0.75;
        x: cx + inner-radius * Math.cos(inner-angle) - dot-size * 0.75 / 2;
        y: cy + inner-radius * Math.sin(inner-angle) - dot-size * 0.75 / 2;
        opacity: 1.0;
    }
    // 尾迹 1
    LoadingDot {
        dot-color: #f53d9e;
        dot-size: dot-size * 0.7;
        x: cx + inner-radius * Math.cos(inner-angle - inner-tail-spacing) - dot-size * 0.7 / 2;
        y: cy + inner-radius * Math.sin(inner-angle - inner-tail-spacing) - dot-size * 0.7 / 2;
        opacity: 0.82;
    }
    // 尾迹 2
    LoadingDot {
        dot-color: #e83db0;
        dot-size: dot-size * 0.65;
        x: cx + inner-radius * Math.cos(inner-angle - inner-tail-spacing * 2) - dot-size * 0.65 / 2;
        y: cy + inner-radius * Math.sin(inner-angle - inner-tail-spacing * 2) - dot-size * 0.65 / 2;
        opacity: 0.64;
    }
    // 尾迹 3
    LoadingDot {
        dot-color: #d43fc0;
        dot-size: dot-size * 0.6;
        x: cx + inner-radius * Math.cos(inner-angle - inner-tail-spacing * 3) - dot-size * 0.6 / 2;
        y: cy + inner-radius * Math.sin(inner-angle - inner-tail-spacing * 3) - dot-size * 0.6 / 2;
        opacity: 0.46;
    }
    // 尾迹 4
    LoadingDot {
        dot-color: #b844cc;
        dot-size: dot-size * 0.55;
        x: cx + inner-radius * Math.cos(inner-angle - inner-tail-spacing * 4) - dot-size * 0.55 / 2;
        y: cy + inner-radius * Math.sin(inner-angle - inner-tail-spacing * 4) - dot-size * 0.55 / 2;
        opacity: 0.3;
    }
    // 尾迹 5
    LoadingDot {
        dot-color: #9a48d4;
        dot-size: dot-size * 0.5;
        x: cx + inner-radius * Math.cos(inner-angle - inner-tail-spacing * 5) - dot-size * 0.5 / 2;
        y: cy + inner-radius * Math.sin(inner-angle - inner-tail-spacing * 5) - dot-size * 0.5 / 2;
        opacity: 0.17;
    }
    // 尾迹 6
    LoadingDot {
        dot-color: #7c4dff;
        dot-size: dot-size * 0.45;
        x: cx + inner-radius * Math.cos(inner-angle - inner-tail-spacing * 6) - dot-size * 0.45 / 2;
        y: cy + inner-radius * Math.sin(inner-angle - inner-tail-spacing * 6) - dot-size * 0.45 / 2;
        opacity: 0.07;
    }

    // ========== 中心脉冲光晕 ==========
    // 外层光晕
    Rectangle {
        x: cx - dot-size * 1.8;
        y: cy - dot-size * 1.8;
        width: dot-size * 3.6;
        height: dot-size * 3.6;
        border-radius: dot-size * 1.8;
        background: primary-color;
        opacity: 0.08 + pulse * 0.12;
    }
    // 内层光晕
    Rectangle {
        x: cx - dot-size * 1.0;
        y: cy - dot-size * 1.0;
        width: dot-size * 2.0;
        height: dot-size * 2.0;
        border-radius: dot-size * 1.0;
        background: secondary-color;
        opacity: 0.15 + pulse * 0.2;
    }
    // 核心点
    Rectangle {
        x: cx - dot-size * 0.35;
        y: cy - dot-size * 0.35;
        width: dot-size * 0.7;
        height: dot-size * 0.7;
        border-radius: dot-size * 0.35;
        background: #ffffff;
        opacity: 0.6 + pulse * 0.4;
    }

    // ========== 闪烁星点(外环周围) ==========
    Sparkle {
        sparkle-color: #ffffff;
        sparkle-size: dot-size * 0.3;
        x: cx + (outer-radius + dot-size * 1.2) * Math.cos(30deg) - self.sparkle-size / 2;
        y: cy + (outer-radius + dot-size * 1.2) * Math.sin(30deg) - self.sparkle-size / 2;
        sparkle-opacity: 0.2 + (1 + Math.sin(pulse-phase * 2 + 30deg)) / 2 * 0.8;
    }
    Sparkle {
        sparkle-color: #4fc3f7;
        sparkle-size: dot-size * 0.25;
        x: cx + (outer-radius + dot-size * 1.4) * Math.cos(110deg) - self.sparkle-size / 2;
        y: cy + (outer-radius + dot-size * 1.4) * Math.sin(110deg) - self.sparkle-size / 2;
        sparkle-opacity: 0.15 + (1 + Math.sin(pulse-phase * 2 + 110deg)) / 2 * 0.7;
    }
    Sparkle {
        sparkle-color: #ffffff;
        sparkle-size: dot-size * 0.35;
        x: cx + (outer-radius + dot-size * 1.1) * Math.cos(200deg) - self.sparkle-size / 2;
        y: cy + (outer-radius + dot-size * 1.1) * Math.sin(200deg) - self.sparkle-size / 2;
        sparkle-opacity: 0.1 + (1 + Math.sin(pulse-phase * 2 + 200deg)) / 2 * 0.9;
    }
    Sparkle {
        sparkle-color: #ff4081;
        sparkle-size: dot-size * 0.28;
        x: cx + (outer-radius + dot-size * 1.3) * Math.cos(290deg) - self.sparkle-size / 2;
        y: cy + (outer-radius + dot-size * 1.3) * Math.sin(290deg) - self.sparkle-size / 2;
        sparkle-opacity: 0.18 + (1 + Math.sin(pulse-phase * 2 + 290deg)) / 2 * 0.75;
    }
    Sparkle {
        sparkle-color: #ffffff;
        sparkle-size: dot-size * 0.22;
        x: cx + (outer-radius + dot-size * 1.5) * Math.cos(160deg) - self.sparkle-size / 2;
        y: cy + (outer-radius + dot-size * 1.5) * Math.sin(160deg) - self.sparkle-size / 2;
        sparkle-opacity: 0.12 + (1 + Math.sin(pulse-phase * 2 + 160deg)) / 2 * 0.6;
    }
    Sparkle {
        sparkle-color: #7c4dff;
        sparkle-size: dot-size * 0.3;
        x: cx + (outer-radius + dot-size * 1.2) * Math.cos(340deg) - self.sparkle-size / 2;
        y: cy + (outer-radius + dot-size * 1.2) * Math.sin(340deg) - self.sparkle-size / 2;
        sparkle-opacity: 0.14 + (1 + Math.sin(pulse-phase * 2 + 340deg)) / 2 * 0.7;
    }

    // ========== 标签文字(带发光效果) ==========
    // 文字光晕
    Text {
        x: -1px;
        y: parent.height - 30px;
        width: parent.width + 2px;
        height: 26px;
        horizontal-alignment: center;
        vertical-alignment: center;
        text: label;
        color: primary-color;
        font-size: 14px;
        opacity: 0.3 + pulse * 0.3;
    }
    // 主文字
    Text {
        x: 0;
        y: parent.height - 30px;
        width: parent.width;
        height: 26px;
        horizontal-alignment: center;
        vertical-alignment: center;
        text: label;
        color: #ffffff;
        font-size: 14px;
        opacity: 0.7 + pulse * 0.3;
    }

    // ========== 动画 Timer ==========
    Timer {
        interval: 16ms;
        triggered => {
            outer-angle = running ? outer-angle + 4deg : outer-angle;
            inner-angle = running ? inner-angle - 6deg : inner-angle;
            pulse-phase = running ? pulse-phase + 3deg : pulse-phase;
        }
    }
}

// ========== 主窗口 ==========
export component MainWindow inherits Window {
    preferred-width: 900px;
    preferred-height: 900px;
    title: "Ring Loading Widget Demo";
    background: #0d1117;

    // 背景装饰光点
    Rectangle {
        x: 100px;
        y: 80px;
        width: 200px;
        height: 200px;
        border-radius: 100px;
        background: @radial-gradient(circle, #4fc3f7 0%, transparent 70%);
        opacity: 0.5;
    }
    Rectangle {
        x: 600px;
        y: 600px;
        width: 250px;
        height: 250px;
        border-radius: 125px;
        background: @radial-gradient(circle, #ff4081 0%, transparent 70%);
        opacity: 0.4;
    }

    VerticalLayout {
        alignment: center;
        spacing: 30px;

        // 标题
        Text {
            text: "✨ Ring Loading Widgets ✨";
            color: #e6edf3;
            font-size: 26px;
            font-weight: 700;
            horizontal-alignment: center;
            letter-spacing: 2px;
        }

        // 主加载组件 --- 大号
        RingLoading {
            ring-size: 160px;
            dot-size: 12px;
            primary-color: #4fc3f7;
            secondary-color: #ff4081;
            track-color: #21262d;
            label: "Loading...";
        }

        // 下方三个不同风格的变体
        HorizontalLayout {
            spacing: 20px;
            alignment: center;

            // 翡翠绿 + 金色
            RingLoading {
                ring-size: 110px;
                dot-size: 9px;
                primary-color: #00e676;
                secondary-color: #ffd740;
                track-color: #1a2e1a;
                label: "Success";
            }

            // 琥珀橙 + 珊瑚红
            RingLoading {
                ring-size: 110px;
                dot-size: 9px;
                primary-color: #ffab40;
                secondary-color: #ff6e40;
                track-color: #2e2314;
                label: "Processing";
            }

            // 紫罗兰 + 靛蓝
            RingLoading {
                ring-size: 110px;
                dot-size: 9px;
                primary-color: #b388ff;
                secondary-color: #8c9eff;
                track-color: #1f1a2e;
                label: "Syncing";
            }
        }
    }
}

5、Cargo.toml

rust 复制代码
[package]
name = "sokoban"
version = "0.1.0"
edition = "2024"

[dependencies]
slint = "1.16.1"

[build-dependencies]
slint-build = "1.16.1"

三、实现原理

本项目的核心是一个使用 Rust + Slint 实现的双环彗星尾加载动画组件 。下面从组件架构、动画原理、数学计算、渲染优化四个维度详细解析其实现原理。

1、组件化架构设计

1.1、 基础组件继承体系

rust 复制代码
// 发光点(带光晕)
component GlowDot inherits Rectangle { ... }

// 普通加载点
component LoadingDot inherits Rectangle { ... }

// sparkle 闪烁点
component Sparkle inherits Rectangle { ... }

// 主环形加载组件
export component RingLoading inherits Rectangle { ... }

设计理念

  • 继承复用 :所有视觉元素都继承自 Rectangle,利用 Slint 的组件继承机制实现样式复用
  • 属性驱动 :通过 in property 定义可配置参数,实现组件的高度可定制化
  • 组合模式RingLoading 组件通过组合多个基础组件构建复杂视觉效果

1.2、 属性系统

rust 复制代码
in property <color> primary-color: #4fc3f7;      // 主色(外环)
in property <color> secondary-color: #ff4081;   // 辅色(内环)
in property <color> track-color: #2a3a4a;       // 轨道颜色
in property <length> ring-size: 120px;          // 环尺寸
in property <length> dot-size: 10px;            // 点尺寸
in property <bool> running: true;               // 运行状态
in property <string> label: "Loading...";       // 标签文本

关键特性

  • 类型安全:Slint 的强类型系统确保属性值的正确性
  • 默认值:所有属性都有合理的默认值,开箱即用
  • 响应式更新:属性变化自动触发界面重绘

2、动画原理与数学计算

2.1、 极坐标系统

组件采用极坐标系统计算点的位置,这是实现环形运动的核心:

rust 复制代码
// 中心点坐标
property <length> cx: self.width / 2;
property <length> cy: (self.height - 36px) / 2;

// 外环半径 & 内环半径
property <length> outer-radius: ring-size / 2;
property <length> inner-radius: ring-size / 3.2;

// 点的极坐标位置计算
x: cx + outer-radius * Math.cos(outer-angle) - dot-size / 2;
y: cy + outer-radius * Math.sin(outer-angle) - dot-size / 2;

数学公式

  • x = cx + r × cos(θ):计算点在水平方向的位置
  • y = cy + r × sin(θ):计算点在垂直方向的位置
  • 角度偏移 :通过 outer-angle - outer-tail-spacing * n 实现彗星尾效果

2.2、 双环反向旋转

rust 复制代码
// 外环:顺时针旋转(角度增加)
outer-angle = running ? outer-angle + 4deg : outer-angle;

// 内环:逆时针旋转(角度减少)
inner-angle = running ? inner-angle - 6deg : inner-angle;

设计意图

  • 视觉层次:内外环反向旋转增加视觉复杂度
  • 速度差异:内环(6°/帧)比外环(4°/帧)更快,形成动态对比
  • 暂停支持running 属性控制动画启停

2.3、 彗星尾渐变算法

rust 复制代码
// 外环彗星尾颜色渐变序列
primary-color → #49b8f0 → #44adf3 → #4499f5 → #4c82f5 → #5968f2 → #6a50ea → #7c3be0 → #8e2ad6

// 尺寸衰减公式
dot-size × (1.0 → 0.94 → 0.88 → 0.82 → 0.76 → 0.7 → 0.64 → 0.58 → 0.52)

// 透明度衰减公式
opacity: 1.0 → 0.88 → 0.74 → 0.58 → 0.43 → 0.3 → 0.18 → 0.09 → 0.04

实现原理

  1. 颜色插值:从主色向深色渐变,模拟运动拖影
  2. 尺寸衰减:尾部点逐渐变小,增强透视感
  3. 透明度衰减:尾部逐渐淡出,实现自然消失效果

2.4 、脉冲呼吸效果

rust 复制代码
// 脉冲相位
property <angle> pulse-phase: 0deg;

// 脉冲值 (0~1 的正弦波)
property <float> pulse: (1 + Math.sin(pulse-phase)) / 2;

// 呼吸值 (用于轨道)
property <float> breathe: 0.3 + pulse * 0.4;

// 内环呼吸值 (反相)
property <float> inner-breathe: 0.7 - pulse * 0.4;

数学原理

  • 正弦函数Math.sin(pulse-phase) 生成平滑的周期性变化
  • 归一化(1 + sin)/2 将值映射到 0, 1 范围
  • 反相效果:外环和内环的呼吸效果相位相反,增强视觉张力

3、 视觉层次与渲染优化

3.1、 多层渲染顺序

复制代码
1. 背景装饰光点(最底层)
2. 外环轨道(带呼吸效果)
3. 内环轨道(反相呼吸效果)
4. 外环彗星尾(8个渐变点 + 光晕)
5. 内环彗星尾(6个渐变点 + 光晕)
6. 中心脉冲光晕(3层叠加)
7. 闪烁星点(6个随机位置)
8. 标签文字(光晕 + 主文字)

渲染策略

  • 从底到顶:确保正确的遮挡关系
  • 性能优化:静态元素(轨道)与动态元素(点)分离
  • GPU 加速:Slint 利用 GPU 进行合成渲染

3.2、 中心光晕的三层结构

rust 复制代码
// 外层光晕(大、低透明度)
Rectangle {
    width: dot-size * 3.6;
    opacity: 0.08 + pulse * 0.12;  // 随脉冲变化
}

// 内层光晕(中、中等透明度)
Rectangle {
    width: dot-size * 2.0;
    opacity: 0.15 + pulse * 0.2;   // 变化幅度更大
}

// 核心点(小、高亮度)
Rectangle {
    width: dot-size * 0.7;
    opacity: 0.6 + pulse * 0.4;    // 最明显的脉冲效果
}

视觉增强

  • 尺寸梯度:3.6x → 2.0x → 0.7x,形成聚焦效果
  • 透明度梯度:外层最淡,核心最亮
  • 同步脉冲 :三层都随 pulse 值变化,但变化幅度不同

3.3 、闪烁星点的随机分布

rust 复制代码
// 6个星点分布在不同的角度和半径上
Sparkle {
    x: cx + (outer-radius + dot-size * 1.2) * Math.cos(30deg) - self.sparkle-size / 2;
    y: cy + (outer-radius + dot-size * 1.2) * Math.sin(30deg) - self.sparkle-size / 2;
    sparkle-opacity: 0.2 + (1 + Math.sin(pulse-phase * 2 + 30deg)) / 2 * 0.8;
}

设计要点

  • 位置随机:30°、110°、200°、290°、160°、340° 等非对称角度
  • 半径变化:1.1x ~ 1.5x 外环半径,增加层次感
  • 相位偏移 :每个星点的脉冲相位不同 (+30deg+110deg 等),实现错峰闪烁

4、性能优化策略

4.1、 定时器优化

rust 复制代码
Timer {
    interval: 16ms;  // ≈60 FPS
    triggered => {
        outer-angle = running ? outer-angle + 4deg : outer-angle;
        inner-angle = running ? inner-angle - 6deg : inner-angle;
        pulse-phase = running ? pulse-phase + 3deg : pulse-phase;
    }
}

性能考虑

  • 60 FPS:16ms 间隔提供流畅的动画体验
  • 条件更新running 为 false 时停止角度计算,节省 CPU
  • 最小化重绘:只更新必要的属性,利用 Slint 的脏标记机制

4.2 、属性计算优化

rust 复制代码
// 预计算中心点,避免重复计算
property <length> cx: self.width / 2;
property <length> cy: (self.height - 36px) / 2;

// 预计算半径
property <length> outer-radius: ring-size / 2;
property <length> inner-radius: ring-size / 3.2;

优化策略

  • 属性缓存:频繁使用的值计算一次并缓存
  • 数学简化:使用常量表达式减少运行时计算
  • 布局分离:尺寸相关计算在布局阶段完成

4.3 、渲染批处理

由于所有视觉元素都是 Rectangle 的派生组件,Slint 可以:

  1. 批量绘制:相同类型的元素合并绘制调用
  2. GPU 实例化:相似几何体使用实例化渲染
  3. 纹理图集:小尺寸元素自动打包到纹理图集

5、可扩展性设计

5.1 、颜色主题系统

主窗口展示了四种预设主题:

rust 复制代码
// 默认:蓝色 + 粉色
primary-color: #4fc3f7
secondary-color: #ff4081

// 翡翠绿 + 金色(Success 主题)
primary-color: #00e676
secondary-color: #ffd740

// 琥珀橙 + 珊瑚红(Processing 主题)
primary-color: #ffab40
secondary-color: #ff6e40

// 紫罗兰 + 靛蓝(Syncing 主题)
primary-color: #b388ff
secondary-color: #8c9eff

5.2 、尺寸自适应

rust 复制代码
// 组件尺寸基于 ring-size 和 dot-size 动态计算
width: ring-size + dot-size * 4 + 30px;
height: ring-size + dot-size * 4 + 50px;

// 所有子元素尺寸都基于这两个基准参数
dot-size * 0.94    // 尾迹点1
dot-size * 3.6      // 外层光晕
ring-size / 3.2     // 内环半径

5.3 、状态管理

rust 复制代码
in property <bool> running: true;      // 动画运行状态
in property <string> label: "Loading..."; // 动态标签

// 应用示例:根据状态切换标签
RingLoading {
    label: running ? "Loading..." : "Paused";
}

6、技术亮点总结

  1. 极坐标数学 :使用 sin/cos 实现精确的环形布局
  2. 彗星尾算法:颜色、尺寸、透明度三重渐变模拟运动轨迹
  3. 脉冲呼吸效果:正弦波驱动多层元素的同步变化
  4. 反向旋转:内外环不同方向和速度增加视觉趣味
  5. 性能优化:60 FPS 定时器 + 属性缓存 + GPU 加速
  6. 完全可配置:颜色、尺寸、速度、标签均可通过属性调整
  7. 组件化设计:基础组件可独立复用,组合成复杂效果

这个加载动画组件不仅视觉效果出色,其实现也展示了 Slint 声明式 UI 在复杂动画场景下的强大表现力。通过纯声明式的代码,实现了通常需要手动 Canvas 绘制才能达到的视觉效果。

相关推荐
Thneonl5 小时前
让恢复动作敢按下去:dry-run、审批门、自动回滚与 mutable twin
rust·kubernetes
程序员爱钓鱼8 小时前
Rust where详解:让泛型与Trait约束更加清晰
前端·后端·rust
Flynt20 小时前
就编译了一下代码,浏览器密码没了——Rust 生态遭遇史上最狠供应链攻击
安全·rust·开源
NutShell Wang1 天前
Rust 1.97 实战迁移:v0 符号重整、Cargo 警告治理与位运算新 API
人工智能·后端·性能优化·rust·vibe coding
进哥AI研习社1 天前
AtomCode 源码编译与二次开发入门:从 Clone 仓库到自定义 Agent 工具的完整指南
rust·二次开发·源码编译·atomcode·agent 工具·tool 扩展·ai 编码智能体
程序员爱钓鱼1 天前
Rust 泛型 Generics详解:编写可复用且类型安全的代码
后端·面试·rust
SomeB1oody2 天前
【RustyML入门】6.0. 数学工具
开发语言·后端·机器学习·rust·教程
今天AI了吗2 天前
从聊天到委派:AI Agent 如何推进长期任务
数据库·人工智能·python·sql·rust