小灰灰搞电子头像
Rust+Slint 实现翻页时钟源码分享封面图

Rust+Slint 实现翻页时钟源码分享

在这里插入图片描述

一、效果展示

在这里插入图片描述

在这里插入图片描述

二、源码分享

1、工程结构

在这里插入图片描述

2、main.rs

slint::include_modules!();

use slint::{Timer, TimerMode};
use std::rc::Rc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

/// 读取当前系统时间,返回6个数字字符串 [H1, H2, M1, M2, S1, S2]
fn current_digits() -> [String; 6] {
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or(Duration::ZERO);
    let total_secs = now.as_secs() + 8*60*60;
    let hours = (total_secs / 3600) % 24;
    let minutes = (total_secs / 60) % 60;
    let seconds = total_secs % 60;
    [
        format!("{}", hours / 10), format!("{}", hours % 10),
        format!("{}", minutes / 10), format!("{}", minutes % 10),
        format!("{}", seconds / 10), format!("{}", seconds % 10),
    ]
}

/// 翻页动画:Rust Timer 逐帧驱动 flap-scale 从 1.0 → 0.0
fn flip_digit(flap: &Rc<dyn Fn(f32)>) {
    flap(1.0);
    const FRAMES: u32 = 22;
    const FRAME_MS: u64 = 16;
    for i in 1..=FRAMES {
        let scale = (i as f32 / FRAMES as f32).powi(2); // ease-in
        let set_value = 1.0 - scale;
        let flap_clone = Rc::clone(flap);
        Timer::single_shot(Duration::from_millis(FRAME_MS * i as u64), move || {
            flap_clone(set_value);
        });
    }
}

macro_rules! anim_setter {
    ($ui:expr, $method:ident) => {{
        let ui_weak = $ui.as_weak();
        Rc::new(move |v: f32| {
            if let Some(ui) = ui_weak.upgrade() { ui.$method(v); }
        }) as Rc<dyn Fn(f32)>
    }};
}

fn main() {
    let ui = MainWindow::new().expect("无法创建主窗口");

    // 初始化:显示当前时间,flap-value 与 value 相同(初始无翻页)
    let initial = current_digits();
    let setters = [
        |ui: &MainWindow, v: &str| { ui.set_digit1_value(v.into()); ui.set_digit1_flap(v.into()); },
        |ui: &MainWindow, v: &str| { ui.set_digit2_value(v.into()); ui.set_digit2_flap(v.into()); },
        |ui: &MainWindow, v: &str| { ui.set_digit3_value(v.into()); ui.set_digit3_flap(v.into()); },
        |ui: &MainWindow, v: &str| { ui.set_digit4_value(v.into()); ui.set_digit4_flap(v.into()); },
        |ui: &MainWindow, v: &str| { ui.set_digit5_value(v.into()); ui.set_digit5_flap(v.into()); },
        |ui: &MainWindow, v: &str| { ui.set_digit6_value(v.into()); ui.set_digit6_flap(v.into()); },
    ];
    for i in 0..6 {
        setters[i](&ui, &initial[i]);
    }

    // 构建每个位的 flap-scale setter
    let flaps: [Rc<dyn Fn(f32)>; 6] = [
        anim_setter!(ui, set_digit1_anim),
        anim_setter!(ui, set_digit2_anim),
        anim_setter!(ui, set_digit3_anim),
        anim_setter!(ui, set_digit4_anim),
        anim_setter!(ui, set_digit5_anim),
        anim_setter!(ui, set_digit6_anim),
    ];

    let ui_weak = ui.as_weak();

    // 启动动画:500ms 后六个数字依次翻页(初始 flap-value == value,视觉上仍为同数字翻页)
    let ui_init = ui_weak.clone();
    let flaps_init = flaps.clone();
    Timer::single_shot(Duration::from_millis(500), move || {
        if ui_init.upgrade().is_none() { return; }
        for (i, delay) in [0u64, 80, 160, 240, 320, 400].iter().enumerate() {
            let flap = Rc::clone(&flaps_init[i]);
            Timer::single_shot(Duration::from_millis(*delay), move || {
                flip_digit(&flap);
            });
        }
    });

    // 每秒更新时间,仅在数字变化时触发翻页
    let mut prev = initial;
    let ui_loop = ui_weak.clone();
    let flaps_loop = flaps;
    let loop_timer = Timer::default();
    loop_timer.start(TimerMode::Repeated, Duration::from_secs(1), move || {
        let new_digits = current_digits();
        if let Some(ui) = ui_loop.upgrade() {
            for i in 0..6 {
                if new_digits[i] != prev[i] {
                    // 关键:先设置 flap-value = 旧值(纸页),再设置 value = 新值(背景)
                    // 然后触发翻页动画
                    let old_val = prev[i].clone();
                    let new_val = new_digits[i].clone();
                    match i {
                        0 => { ui.set_digit1_flap(old_val.into()); ui.set_digit1_value(new_val.into()); }
                        1 => { ui.set_digit2_flap(old_val.into()); ui.set_digit2_value(new_val.into()); }
                        2 => { ui.set_digit3_flap(old_val.into()); ui.set_digit3_value(new_val.into()); }
                        3 => { ui.set_digit4_flap(old_val.into()); ui.set_digit4_value(new_val.into()); }
                        4 => { ui.set_digit5_flap(old_val.into()); ui.set_digit5_value(new_val.into()); }
                        5 => { ui.set_digit6_flap(old_val.into()); ui.set_digit6_value(new_val.into()); }
                        _ => {}
                    }
                    flip_digit(&flaps_loop[i]);
                }
            }
        }
        prev = new_digits;
    });

    ui.run().expect("运行 UI 失败");
}


3、build.rs

fn main(){
    slint_build::compile("ui/main.slint")
        .unwrap()
}

4、main.slint

// ==================== 翻页数字组件 ====================
component FlipDigit {
    in property <string> value: "0";         // 新值(背景显示,翻页完成后露出)
    in property <string> flap-value: "0";    // 旧值(翻页纸页显示,翻走后消失)
    in property <length> digit-width: 80px;
    in property <length> digit-height: 130px;

    // 动画属性:Rust 逐帧驱动 1.0 → 0.0
    in property <float> flap-scale: 1.0;

    // ==================== 卡片容器 ====================
    Rectangle {
        width: root.digit-width;
        height: root.digit-height;
        border-radius: 10px;
        background: #1c1c1e;

        // 底层:显示新值(翻页完成后完全露出)
        Text {
            text: root.value;
            font-size: root.digit-height * 0.82;
            font-weight: 700;
            color: #ff9f0a;
            horizontal-alignment: center;
            vertical-alignment: center;
            width: parent.width;
            height: parent.height;
        }

        // 翻页纸页:显示旧值,从顶部往下翻走
        Rectangle {
            width: root.digit-width;
            height: root.digit-height;
            border-radius: 10px;
            background: #2c2c2e;
            transform-origin: { x: parent.width / 2, y: 0 };
            transform-scale-y: root.flap-scale * 100%;

            // 纸页上显示旧值
            Text {
                text: root.flap-value;
                font-size: root.digit-height * 0.82;
                font-weight: 700;
                color: #ff9f0a;
                horizontal-alignment: center;
                vertical-alignment: center;
                width: parent.width;
                height: parent.height;
            }

            // 翻页底边阴影
            Rectangle {
                y: parent.height - 12px;
                height: 12px;
                width: parent.width;
                background: #00000040;
            }
        }
    }
}

// ==================== 主窗口 ====================
export component MainWindow inherits Window {
    title: "翻页时钟";
    preferred-width: 620px;
    preferred-height: 320px;
    background: #0a0a0a;

    // 6个新值
    in property <string> digit1-value: "0";
    in property <string> digit2-value: "0";
    in property <string> digit3-value: "0";
    in property <string> digit4-value: "0";
    in property <string> digit5-value: "0";
    in property <string> digit6-value: "0";

    // 6个旧值(翻页纸页显示)
    in property <string> digit1-flap: "0";
    in property <string> digit2-flap: "0";
    in property <string> digit3-flap: "0";
    in property <string> digit4-flap: "0";
    in property <string> digit5-flap: "0";
    in property <string> digit6-flap: "0";

    // 6个 flap-scale:Rust 逐帧驱动
    in property <float> digit1-anim: 1.0;
    in property <float> digit2-anim: 1.0;
    in property <float> digit3-anim: 1.0;
    in property <float> digit4-anim: 1.0;
    in property <float> digit5-anim: 1.0;
    in property <float> digit6-anim: 1.0;

    VerticalLayout {
        alignment: center;

        Text {
            text: "FLIP  CLOCK";
            font-size: 11px;
            color: #555555;
            horizontal-alignment: center;
            letter-spacing: 4px;
            height: 22px;
            vertical-alignment: center;
        }

        HorizontalLayout {
            alignment: center;
            spacing: 8px;
            height: 130px;

            FlipDigit { value: digit1-value; flap-value: digit1-flap; flap-scale: digit1-anim; }
            FlipDigit { value: digit2-value; flap-value: digit2-flap; flap-scale: digit2-anim; }

            VerticalLayout {
                alignment: center;
                spacing: 22px;
                width: 20px;
                Rectangle { width: 9px; height: 9px; border-radius: 5px; background: #ff9f0a; }
                Rectangle { width: 9px; height: 9px; border-radius: 5px; background: #ff9f0a; }
            }

            FlipDigit { value: digit3-value; flap-value: digit3-flap; flap-scale: digit3-anim; }
            FlipDigit { value: digit4-value; flap-value: digit4-flap; flap-scale: digit4-anim; }

            VerticalLayout {
                alignment: center;
                spacing: 22px;
                width: 20px;
                Rectangle { width: 9px; height: 9px; border-radius: 5px; background: #ff9f0a; }
                Rectangle { width: 9px; height: 9px; border-radius: 5px; background: #ff9f0a; }
            }

            FlipDigit { value: digit5-value; flap-value: digit5-flap; flap-scale: digit5-anim; }
            FlipDigit { value: digit6-value; flap-value: digit6-flap; flap-scale: digit6-anim; }
        }

        Text {
            text: digit1-value + digit2-value + " : " + digit3-value + digit4-value + " : " + digit5-value + digit6-value;
            font-size: 13px;
            color: #444444;
            horizontal-alignment: center;
            height: 26px;
            vertical-alignment: center;
        }
    }
}


5、Cargo.toml

[package]
name = "modbus-debug-assistant"
version = "0.1.0"
edition = "2024"

[dependencies]
slint = "1.17.1"


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

三、实现原理

本项目基于 Rust + Slint 实现了一个翻页时钟,核心思路是:用 Slint 声明式 UI 描述“翻页卡片”的静态结构,用 Rust 的 Timer 逐帧驱动动画属性,从而模拟出纸页从上往下翻走的视觉效果。下面从数据流、动画机制、UI 结构三个层面展开说明。

1、整体数据流

整个程序围绕 6 个数字位(时 2 位、分 2 位、秒 2 位)运转,每个数字位都对应一组“新值 / 旧值 / 动画进度”三个属性:

系统时间 → current_digits() → 6 个数字字符串
        → 每秒 Timer 对比新旧值
        → 数字变化时:flap-value = 旧值,value = 新值
        → 触发 flip_digit() 逐帧驱动 flap-scale 1.0 → 0.0
        → Slint 根据 flap-scale 缩放纸页,完成翻页
  • value:新数字,作为翻页完成后的背景显示;
  • flap-value:旧数字,显示在“纸页”上,翻页过程中被翻走;
  • flap-scale:纸页的纵向缩放比例,从 1.0(完全展开)渐变到 0.0(完全翻走)。

2、时间读取与数字拆分

current_digits() 负责读取系统时间并拆成 6 个字符:

fn current_digits() -> [String; 6] {
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or(Duration::ZERO);
    let total_secs = now.as_secs() + 8*60*60;   // 东八区偏移
    let hours = (total_secs / 3600) % 24;
    let minutes = (total_secs / 60) % 60;
    let seconds = total_secs % 60;
    [
        format!("{}", hours / 10), format!("{}", hours % 10),
        format!("{}", minutes / 10), format!("{}", minutes % 10),
        format!("{}", seconds / 10), format!("{}", seconds % 10),
    ]
}

关键点:

  • 通过 SystemTime::now() 获取自 UNIX 纪元以来的秒数,加上 8*60*60 得到东八区时间;
  • 用整除和取模分别拆出十位与个位,例如 hours / 10 得到十位、hours % 10 得到个位;
  • 返回 [String; 6] 数组,正好对应 UI 中的 6 个 FlipDigit 组件。

3、翻页动画的驱动机制

翻页动画的核心是 flip_digit() 函数,它用 Rust 的 Timer::single_shot 逐帧驱动 flap-scale 从 1.0 渐变到 0.0:

fn flip_digit(flap: &Rc<dyn Fn(f32)>) {
    flap(1.0);
    const FRAMES: u32 = 22;
    const FRAME_MS: u64 = 16;
    for i in 1..=FRAMES {
        let scale = (i as f32 / FRAMES as f32).powi(2); // ease-in
        let set_value = 1.0 - scale;
        let flap_clone = Rc::clone(flap);
        Timer::single_shot(Duration::from_millis(FRAME_MS * i as u64), move || {
            flap_clone(set_value);
        });
    }
}

实现要点:

  • 22 帧、每帧 16ms,总时长约 352ms,形成流畅的翻页动画;
  • 使用 (i / FRAMES).powi(2) 做 ease-in 缓动,让纸页先慢后快,更接近真实翻页手感;
  • 每一帧都通过 Timer::single_shot 延迟触发,把 flap-scale 的新值写回 Slint 属性;
  • flap 是一个 Rc<dyn Fn(f32)> 闭包,由 anim_setter! 宏生成,负责调用 Slint 生成的 setter 方法。

anim_setter! 宏的作用是把 Slint 组件的弱引用和 setter 方法封装成一个可调用的闭包:

macro_rules! anim_setter {
    ($ui:expr, $method:ident) => {{
        let ui_weak = $ui.as_weak();
        Rc::new(move |v: f32| {
            if let Some(ui) = ui_weak.upgrade() { ui.$method(v); }
        }) as Rc<dyn Fn(f32)>
    }};
}

使用弱引用 as_weak() 避免循环引用导致的内存泄漏;闭包被包装成 Rc<dyn Fn(f32)>,方便在 Timer 回调中克隆和传递。

4、每秒刷新与翻页触发

主循环使用 Timer::default() 创建重复定时器,每秒执行一次:

let loop_timer = Timer::default();
loop_timer.start(TimerMode::Repeated, Duration::from_secs(1), move || {
    let new_digits = current_digits();
    if let Some(ui) = ui_loop.upgrade() {
        for i in 0..6 {
            if new_digits[i] != prev[i] {
                // 先设置 flap-value = 旧值(纸页),再设置 value = 新值(背景)
                let old_val = prev[i].clone();
                let new_val = new_digits[i].clone();
                match i {
                    0 => { ui.set_digit1_flap(old_val.into()); ui.set_digit1_value(new_val.into()); }
                    // ... 其余 5 位同理
                    _ => {}
                }
                flip_digit(&flaps_loop[i]);
            }
        }
    }
    prev = new_digits;
});

这里有一个关键设计:

  1. 先判断当前位数字是否真的变化(new_digits[i] != prev[i]),只有变化才触发翻页,避免无意义的动画;
  2. 变化时,先把旧值写入 flap-value(纸页),再把新值写入 value(背景),这样翻页过程中纸页显示旧数字、背景显示新数字;
  3. 最后调用 flip_digit() 驱动纸页从 1.0 缩放到 0.0,视觉上旧数字被“翻走”,露出新数字。

5、Slint 侧的翻页卡片结构

main.slint 中定义了 FlipDigit 组件,核心结构如下:

component FlipDigit {
    in property <string> value: "0";         // 新值(背景)
    in property <string> flap-value: "0";    // 旧值(纸页)
    in property <float> flap-scale: 1.0;     // 动画进度

    Rectangle {
        // 底层:显示新值
        Text { text: root.value; /* ... */ }

        // 翻页纸页:显示旧值,从顶部往下翻走
        Rectangle {
            transform-origin: { x: parent.width / 2, y: 0 };
            transform-scale-y: root.flap-scale * 100%;
            Text { text: root.flap-value; /* ... */ }
        }
    }
}

翻页效果的视觉实现依赖两个关键属性:

  • transform-origin:设置为 { x: parent.width / 2, y: 0 },即以卡片顶部中心为旋转/缩放原点,这样纸页会从顶部开始往下翻;
  • transform-scale-y:绑定 root.flap-scale * 100%,当 Rust 侧把 flap-scale 从 1.0 驱动到 0.0 时,纸页的纵向高度逐渐缩小,最终完全消失,露出底层的“新值”。

6、启动时的初始翻页

程序启动 500ms 后,会先让 6 个数字依次做一次翻页动画,作为开场效果:

Timer::single_shot(Duration::from_millis(500), move || {
    if ui_init.upgrade().is_none() { return; }
    for (i, delay) in [0u64, 80, 160, 240, 320, 400].iter().enumerate() {
        let flap = Rc::clone(&flaps_init[i]);
        Timer::single_shot(Duration::from_millis(*delay), move || {
            flip_digit(&flap);
        });
    }
});

由于初始化时 flap-value 与 value 相同,所以这次翻页在视觉上表现为“同数字翻页”,营造出时钟启动的仪式感。每个数字之间间隔 80ms,形成从左到右的波浪式翻页效果。

7、小结

模块职责
current_digits()读取系统时间并拆分为 6 个数字字符
flip_digit()用 22 帧 Timer 逐帧驱动 flap-scale 从 1.0 → 0.0
anim_setter! 宏把 Slint setter 封装为可克隆的闭包
每秒 Timer对比新旧时间,仅在数字变化时触发翻页
FlipDigit 组件用 transform-origin + transform-scale-y 实现纸页翻走效果

通过 Rust 与 Slint 的分工协作——Rust 负责时间计算与动画驱动,Slint 负责声明式 UI 与视觉呈现——最终实现了流畅的翻页时钟效果。

在这里插入图片描述

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_15181569/article/details/164224640

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--