首页
/ Slint 状态系统实战:用 states 与 animate 打造精致的明暗主题切换开关(fancy-switches 示例深度解析)

Slint 状态系统实战:用 states 与 animate 打造精致的明暗主题切换开关(fancy-switches 示例深度解析)

2026-09-12 16:33:06作者:劳婵绚Shirley

导读

本文基于 Slint 开源仓库中的 examples/fancy-switches 示例,深入讲解如何利用 Slint 的状态系统(states)过渡动画(transitions/in 块)以及缓动曲线(easing),从零构建两款细节丰富的明暗主题切换开关:DarkModeSwitch(日月开关)与 SunMoonSwitch(昼夜天空开关)。读完本文,你将掌握 enum 状态建模、TouchArea 交互绑定、states 属性覆盖、animate 过渡与 spring/cubic-bezier 缓动的完整用法,并能在自己的项目中复刻同类精致 UI 组件。

该示例目录包含两个核心组件文件 DarkModeSwitch.slintSunMoonSwitch.slint,以及将它们组合到同一窗口的 demo.slintDarkModeSwitch 的设计灵感源自 Shmelt Studios 的教程,属于典型的“太阳/月亮滑块”开关;SunMoonSwitch 则在此基础上加入了天空、云朵与星星的场景化过渡。


一、示例概览:两个开关组件与它们的组合方式

1.1 文件结构与资源

examples/fancy-switches/ 目录下包含:

文件 作用
demo.slint 入口窗口,将两个开关并排展示
DarkModeSwitch.slint 太阳/月亮滑块开关(含 SunIconRayThickRayThin 辅助组件)
SunMoonSwitch.slint 昼夜天空开关(含 SunMoonThumb 辅助组件与 Utils 全局函数)
images/ 位图与矢量资源:switch.pngline.pngmoon.svgclouds-background.pngclouds-front.pngstars.pngshadow-frame.png

图片资源通过 @image-url(...) 引用,既支持 PNG 位图也支持 SVG 矢量图(如 moon.svg)。

1.2 入口窗口:组件复用与布局

demo.slint 演示了 Slint 最基础的模块化用法——用 import 语句从其他 .slint 文件导入组件,然后在 Window 内以元素方式实例化:

import { DarkModeSwitch } from "DarkModeSwitch.slint";
import { SunMoonSwitch } from "SunMoonSwitch.slint";

export component AppWindow inherits Window {
    preferred-width: 600px;
    preferred-height: 450px;
    background: #e3e3e3;

    DarkModeSwitch {
        x: 200px;
        y: 100px;
    }

    SunMoonSwitch {
        x: 200px;
        y: 250px;
    }
}

这里展示了三个要点:

  • 跨文件组件导入import { ComponentName } from "文件名.slint",文件名基于当前 .slint 文件所在目录解析;
  • 组件像元素一样使用:导出的组件可直接作为元素放入布局,并像普通元素一样设置 x/y 定位;
  • preferred-width/preferred-height:设置窗口的期望尺寸,background 直接给根窗口上色。

该示例也被纳入了仓库的自动化测试:在 tests/driver/interpreter/build.rs 中注册为 ("example_fancy_switches", "examples/fancy-switches/demo.slint"),说明 demo.slint 可由解释器(interpreter)直接加载执行,你可以在本地用 slint-viewer 或在线编辑器直接预览。


二、核心机制之一:用 enum 建模开关状态

两个开关都使用自定义枚举类型作为状态变量,这是 Slint 声明式状态设计的经典模式。

DarkModeSwitch.slint 中:

enum Theme { light, dark }
export component DarkModeSwitch {
    property <Theme> theme: Theme.light;
    ...
}

SunMoonSwitch.slint 中:

enum Theme { day, night }
export component SunMoonSwitch {
    property <Theme> theme: Theme.night;
    ...
}

要点:

  • enum 声明位于组件外部、文件顶层,可被多个组件共享;
  • property <Theme> theme: Theme.light 声明了一个带默认值的可读写属性,默认值是 Theme.light(白天/亮色);
  • 该属性未加 in/out/private 限定符,因此是组件的公共属性。外部代码(如 Rust/C++/JS 后端或父组件)可以通过这个属性直接读取、切换或驱动主题,这让“开关组件”天然具备可复用的双向接口——既能在 .slint 内部用 TouchArea 切换,也能从宿主语言侧读写。

从源码结构看,这种“用 enum 属性 + states 条件匹配”的组合,正是 Slint 官方文档 States and Transitions 所描述的推荐状态建模方式:状态条件可以是任意布尔表达式,而枚举比较(root.theme == Theme.dark)是最清晰、可读性最高的一种。


三、核心机制之二:交互触发——TouchArea

状态切换的“扳机”是 TouchAreaclicked 回调。在 DarkModeSwitch.slint 中:

TouchArea {
    clicked => {
        if root.theme == Theme.light {
            root.theme = Theme.dark;
        } else {
            root.theme = Theme.light;
        }
    }
}

SunMoonSwitch 的结构完全一致,只是比较的是 Theme.day/Theme.night(见 SunMoonSwitch.slint)。

这段代码体现了 Slint 的响应式模型:clicked => { ... } 是事件处理语法,回调内修改 root.theme 属性后,所有依赖该属性的绑定都会自动重新求值,而 states 块会依据新值选择激活的状态,进而触发对应的过渡动画——无需任何手动刷新或 diff 计算

注意:SunMoonSwitchtheme 属性是 property <Theme> theme: Theme.night;,即声明为 in-out(默认)语义的可读写属性;如果只想对外暴露只读状态,可以改用 inout 限定符,并让父组件控制。


四、核心机制之三:states 块与属性覆盖

states 是 Slint 状态系统的核心语法。一个 states [ ... ] 块定义若干具名状态,每个状态包含一个可选 when 条件和一组属性赋值;当条件满足时,这些属性值生效并覆盖元素原本的绑定。同一时刻至多一个状态激活,多个条件同时满足时按源码顺序取第一个。

4.1 DarkModeSwitch 的两个状态

DarkModeSwitch.slint 定义了 darkModelightMode 两个状态:

states [
    darkMode when root.theme == Theme.dark: {
        thumb.x: thumb.y;
        thumb.background: @radial-gradient(circle,#b0b0b0 0%, #cccccc 70%, #e9e9e9 100%);
        frameBacker.background: #2A2A2A;
        moon.transform-rotation: -20deg;
        sun.color: #fc7a10;
        sun.transform-scale: 0.8;
        in {
            animate thumb.x, thumb.background, frameBacker.background, sun.color {
                duration: 200ms;
                easing: ease-out-sine;
            }
            animate moon.transform-rotation, sun.transform-scale {
                duration: 1200ms;
                easing: ease-out-elastic;
            }
        }
    }
    lightMode when root.theme == Theme.light: {
        thumb.x: frame.width - thumb.width - thumb.y;
        thumb.background: @radial-gradient(circle,#515151 0%, #242424 80%, #191919 100%);
        frameBacker.background: transparent;
        moon.transform-rotation: 20deg;
        sun.color: #2A2A2A;
        sun.transform-scale: 1;
        in {
            animate thumb.x, thumb.background, frameBacker.background, moon.transform-rotation {
                duration: 200ms;
                easing: ease-out-sine;
            }
            animate sun.transform-scale {
                duration: 1200ms;
                easing: ease-out-elastic;
            }
            animate sun.color {
                duration: 1000ms;
            }
        }
    }
]

逐行拆解其中的设计技巧:

  • 限定名(qualified name)属性赋值thumb.xsun.colorframeBacker.background 等均通过元素 id 定位子元素,这正是 states-and-transitions.mdx 中“限定名指向后代元素属性”的用法;thumb.x 属于该元素自身属性,也可写作 root.thumb.x
  • @radial-gradient 径向渐变:thumb 在两种主题下使用截然相反的渐变方向(暗色主题下亮灰球,亮色主题下暗灰球),营造“滑块本身跟着主题变色”的物理质感;
  • moon.transform-rotationsun.transform-scale:月亮图标在切换时旋转 ±20deg,太阳圆盘缩放 0.8 ↔ 1,让两个图标“有生命感”;
  • drop-shadow-* 投影属性(见组件定义处 DarkModeSwitch.slint):drop-shadow-offset-x/ydrop-shadow-colorblack.transparentize(30%) 表示 30% 透明度的黑色)、drop-shadow-blur 组合出 thumb 悬浮于轨道之上的立体感。

4.2 状态内部的过渡动画:in { animate ... }

注意上面每个状态里都有一个 in { ... } 块。这是 Slint 状态过渡(transition) 语法:in 表示进入该状态时触发的动画,out 表示离开该状态时的动画,in-out 两者都触发(也接受拼写 in_out)。根据官方文档 states-and-transitions.mdx

  • 一个方向块内只能包含 animate 子句;
  • animate 属性列表 { duration: ...; easing: ...; } 可以一次列出多个属性(逗号分隔),等价于为每个属性分别声明动画;
  • animate 字段可选:delaydurationeasingiteration-countdirectionenabled,见 animations.mdx
  • 可动画属性类型限于 intfloatlengthphysical-lengthcolorbrushangle——本示例中运动的 thumb.x(length)、background(brush)、transform-rotation(angle)、transform-scale(float)、sun.color(brush)全部符合;
  • 同一属性不能同时被多个 animate 块驱动,也不能在已有自身 animate 绑定的情况下再被过渡动画驱动。

DarkModeSwitch 的缓动设计非常讲究:

  • 200ms + ease-out-sine:用于 thumb.xbackgroundframeBacker.background 等“滑动 + 变色”类属性,短促且平滑,符合滑块物理直觉;
  • 1200ms + ease-out-elastic:用于 moon.transform-rotationsun.transform-scale,弹性缓动让月亮/太阳带一点“弹跳回位”的俏皮感;
  • 1000ms(默认线性):用于 sun.color 的渐变色过渡,缓和而自然。

通过“短时平滑 + 长时弹性”的组合,同一个点击动作里不同属性以不同节奏运动,这正是“fancy”(精致)质感的关键来源。

4.3 状态的语义细节

结合 states-and-transitions.mdx 的规范,可以归纳本示例遵守的几条规则:

  • 状态名是元素作用域内的标识符;when 后跟任意布尔表达式,可引用作用域内任何属性(本示例引用 root.theme);
  • 未列在激活状态里的属性保持其正常绑定——因此 thumb 的尺寸、图标位置等基础布局属性都写在组件定义处,而只有“随主题变化”的属性才放进状态;
  • 不带 when 的状态永远不会被自动选中,仅作为过渡可引用的名字——本示例两个状态都带 when,属于完整定义。

五、SunMoonSwitch 深度剖析:场景化状态过渡

如果说 DarkModeSwitch 是“滑块 + 图标”的经典方案,那么 SunMoonSwitch 则把整个开关变成了一个微型昼夜场景:天空变色、云朵滑动、星星浮现、太阳/月亮在轨道上互换。

5.1 全局纯函数 Utils.MapRange

SunMoonSwitch.slint 定义了全局函数:

global Utils {
    public pure function MapRange(value: length, inMin: length, inMax: length, outMin: length, outMax: length) -> length {
        return clamp(outMin, (value - inMin) * (outMax - outMin) / (inMax - inMin) + outMin, outMax);
    }
}

这是经典的数值映射工具函数:把 value[inMin, inMax] 线性映射到 [outMin, outMax],并用 clamp 限制输出范围。global 声明了模块级单例,public pure function 表示可被组件任意调用且无副作用。它在 SunMoonThumb 中被用来根据滑块位置反向计算月亮的位置(见下文 5.2)。从源码结构看,该函数被定义为 public,也可以被同一文件内其他组件甚至外部导入复用。

5.2 SunMoonThumb:轨道上的日月渐变

SunMoonThumbSunMoonSwitch.slint)是滑块本体,核心结构如下:

component SunMoonThumb {
    in property <length> thumb-position: 50px;

    Rectangle {
        width: 200px;
        height: 100px;
        border-radius: self.height / 2;
        clip: true;
        Rectangle {
            width: 0px;
            height: 0px;
            x: root.thumb-position;
            // 三层白色半透明圆环:营造光晕/轨道层次
            Rectangle { width: 260px; ... background: white; opacity: 0.05; }
            Rectangle { width: 200px; ... background: white; opacity: 0.05; }
            Rectangle { width: 140px; ... background: white; opacity: 0.05; }

            clipper := Rectangle {
                width: 85px;
                height: self.width;
                border-radius: self.width / 2;
                clip: true;

                sun := Rectangle {
                    width: 85px;
                    background: @radial-gradient(circle,#ffce08 0%, #fdd224 80%, #fce37f 100%);
                    ...
                }
                moon := Rectangle {
                    x: Utils.MapRange(root.thumb-position, 50px, 100px, 85px, 0px);
                    width: 85px;
                    background: @radial-gradient(circle,#bcbcbc 0%, #e7e7e7 80%, #ffffff 100%);
                    ...
                }
            }
        }
    }
}

设计亮点:

  • in property <length> thumb-position: 50pxin 表示该属性只允许被父组件(状态块)写入,SunMoonThumb 自身不修改它;
  • 坐标原点在元素左上角:通过把“空尺寸”的外层 Rectanglex 设为 root.thumb-position,其内部内容整体随之平移;
  • MapRange 的巧用sun 固定在滑块内,moonxthumb-position[50px, 100px] 区间反向映射到 [85px, 0px]——也就是说,随着滑块从左(日)到右(夜)移动,月亮从右侧滑入、把太阳“顶”出视野,两球在轨道上无缝互换;
  • 三层白色半透明圆环 + clip: trueclip 裁剪让圆环只显示在滑块范围内,叠加出细腻的光晕层次;
  • 渐变球体:太阳用 #ffce08 → #fdd224 → #fce37f 的暖黄渐变,月亮用 #bcbcbc → #e7e7e7 → #ffffff 的冷灰渐变,视觉上区分昼夜。

5.3 场景图层:天空、云朵、星星与阴影

开关主体(SunMoonSwitch.slint)由多层元素堆叠而成:

export component SunMoonSwitch {
    property <Theme> theme: Theme.night;
    width: 200px;
    height: 100px;

    frameBacker := Rectangle {        // 底色背景(随主题变色)
        width: parent.width;
        height: parent.height;
        background: #1e2232;
        border-radius: self.height / 2;
    }

    Rectangle {                       // 场景容器:裁剪 + 圆角
        width: parent.width;
        height: parent.height;
        clip: true;
        border-radius: self.height / 2;

        clouds-background := Image { x: 14px; y: -6px; width: 202px;
            source: @image-url("images/clouds-background.png"); }
        clouds-foreground := Image { x: 30px; y: 5px; width: 202px;
            source: @image-url("images/clouds-front.png"); }
        stars := Image { x: 15px; y: 15px; width: 80px;
            source: @image-url("images/stars.png"); }
    }

    Image {                           // 顶部阴影框:增强内凹立体感
        x: -1px;
        width: 202px;
        source: @image-url("images/shadow-frame.png");
    }

    thumb := SunMoonThumb { }

    TouchArea {
        clicked => { /* 切换 day/night */ }
    }
}

这里体现了位图资源与矢量元素混用的典型姿势:天空背景用 PNG 云朵(前后两层形成视差),星星用 PNG 贴图,而月亮用 SVG(moon.svgDarkModeSwitch 中)。前后两层云 clouds-background/clouds-foreground 在状态动画中以不同速度、不同缓动移动,模拟出远景与近景的层次感。

5.4 day / night 状态与错落有致的动画节奏

SunMoonSwitch.slint 定义了 nightModedayMode 两个状态,动画设计明显比 DarkModeSwitch 更“电影化”:

states [
    nightMode when root.theme == Theme.night: {
        thumb.thumb-position: root.width - thumb.width - 50px;
        frameBacker.background: #1e2232;
        clouds-background.y: 120px;
        clouds-foreground.y: 120px;
        in {
            animate frameBacker.background, stars.y, stars.opacity {
                easing: ease-out-sine;
                duration: 200ms;
            }
            animate thumb.thumb-position {
                easing: cubic-bezier(0.61, 0.21, 0.68, 1.22);
                duration: 300ms;
            }
            animate clouds-background.y, clouds-foreground.y {
                easing: ease-in-sine;
                duration: 150ms;
            }
        }
    }
    dayMode when root.theme == Theme.day: {
        thumb.thumb-position: 50px;
        frameBacker.background: #3d85ba;
        stars.y: -60px;
        stars.opacity: 0.4;
        in {
            animate frameBacker.background, stars.y, stars.opacity {
                easing: ease-out-sine;
                duration: 200ms;
            }
            animate thumb.thumb-position {
                easing: cubic-bezier(0.61, 0.21, 0.68, 1.22);
                duration: 300ms;
            }
            animate clouds-background.y {
                easing: ease-out-sine;
                duration: 300ms;
            }
            animate clouds-foreground.y {
                easing: cubic-bezier(0.61, 0.21, 0.68, 1.22);
                duration: 350ms;
            }
        }
    }
]

值得细品的动画编排:

  • thumb.thumb-positioncubic-bezier(0.61, 0.21, 0.68, 1.22) + 300ms:这是一条 y 值超过 1 的贝塞尔曲线,即“先轻微后退再冲向前方”的过冲效果,让滑块位移带一点果冻般的弹性,比线性滑动更有生命力;
  • 云朵双速运动nightMode 中两层云都以 150ms 快速“消失”(y 移到 120px),而 dayMode 中背景云 300ms、前景云 350ms 且缓动不同——前景比背景慢半拍,形成视差延迟感;
  • stars.ystars.opacity 联动:夜晚星星滑入视野(默认位置),白天星星上移到 -60px 且透明度降到 0.4opacity 可动画(float 类型),实现星星“升起 + 隐退”;
  • frameBacker.background#1e2232(深夜蓝)与 #3d85ba(晴空蓝)之间 200ms 过渡,天空变色的同时云朵和星星各自忙碌,整个组件像一段微缩的昼夜延时摄影。

注意一个细节:nightMode 中星星默认就在视野内(未显式指定 stars.y 时保持组件定义处的绑定值),而 dayMode 才把它推上去;两个状态对 stars.opacity 的设置也只在 dayMode 出现。这再次印证了状态系统的“属性未列出则保持正常绑定”语义。


六、动画与缓动语法速查(本示例用到的全部知识点)

把本示例用到的动画能力对照 animations.mdx 官方规范整理如下,便于直接复用:

6.1 animate 字段一览

字段 类型 默认值 说明
delay duration 动画开始前的等待时间
duration duration 动画完成所需时间
easing easing 缓动曲线(见 6.2)
iteration-count float 1 播放次数,负数表示无限循环,允许小数
direction enum 每轮迭代的播放方向(如 alternatealternate-reverse
enabled bool true false 时直接跳到目标值,不做动画

6.2 本示例用到的缓动曲线

缓动 使用位置 效果特征
ease-out-sine 两个开关中的滑块/变色/星星动画 快启动、慢收尾的正弦缓出,平滑自然
ease-in-sine SunMoonSwitch 云朵消失 慢启动、快收尾,配合“云被风吹走”的意象
ease-out-elastic DarkModeSwitch 日月旋转/缩放 带多次回弹的弹性缓出,俏皮
cubic-bezier(0.61, 0.21, 0.68, 1.22) SunMoonSwitch 滑块位移 自定义贝塞尔,y 值可越过 1 实现过冲弹跳

6.3 过渡方向块

  • in { animate ... }:进入该状态时播放;
  • out { animate ... }:离开该状态时播放;
  • in-out { animate ... }(也接受 in_out):两个方向都播放;
  • 一个状态可含多个方向块;animate * { ... } 可匹配该状态修改的所有属性(仅在过渡内合法);
  • 过渡动画中属性名可以使用限定名(如 animate root.background),这是与元素自身 animate 绑定的关键区别。

6.4 可动画属性类型

intfloatlengthphysical-lengthcolorbrushangle 可动画。因此本示例中运动的全部是 length(位置)、brush(背景/颜色)、angle(旋转)、float(缩放/透明度),而对 bool/string/image 属性的动画会直接编译报错。


七、运行与预览方式

7.1 本地预览

仓库为示例提供了解释器驱动的运行方式。demo.slinttests/driver/interpreter/build.rs 中被注册,说明它可以直接被 Slint 解释器加载。常见本地预览命令:

# 使用 slint-viewer 直接预览(需先构建或安装)
cargo run -p slint-viewer -- examples/fancy-switches/demo.slint

也可以参照仓库中其他示例的常规做法,把 demo.slint 通过 slint::include_modules!() 或解释器 API 嵌入任意 Rust/C++/JS/Python 宿主程序。该组件没有外部依赖与平台特定代码,因此在 winit、Qt、嵌入式等各类后端上均可运行。

7.2 在宿主语言中驱动 theme 属性

两个开关都暴露了 theme 公共属性,因此从 Rust 侧可以像读写普通组件属性一样控制它,例如在 Rust 中通过 weak_handleset_theme 之类生成的访问器在运行时切换开关状态,进而让“开关组件”与业务逻辑解耦。需要说明的是,具体生成的 API 名称由编译宏决定,宿主语言绑定可参考仓库的 api/cppapi/nodeapi/python 目录中的相应示例。


八、小结:从本示例可以学到的设计模式

  1. 状态建模:用 enum 属性 + states [ name when condition: { ... } ] 描述离散 UI 状态,条件表达式与属性覆盖让状态语义一目了然;
  2. 交互解耦TouchArea.clicked 只负责翻转枚举属性,所有视觉响应全部由绑定与状态自动完成;
  3. 动画分层:同一状态内为不同属性配置不同时长与缓动(如“滑块 300ms 过冲、云朵 150ms 快移、星星 200ms 淡入”),营造出丰富而有层次的动效;
  4. 资源混排@image-url 可同时加载 PNG 与 SVG,位图负责细腻质感(云、星、阴影框),矢量图负责可缩放图标(月亮);
  5. 复用与组合global 纯函数(Utils.MapRange)与内聚的辅助组件(SunIconSunMoonThumb)保持单一职责,import 让组件跨文件复用,最终由 demo.slint 一键组合演示。

如果你正在用 Slint 开发设置页、深浅色主题切换或任何需要“状态 + 过渡动画”的控件,fancy-switches 是一个非常完整的参考实现——从枚举状态到贝塞尔过冲、从位图分层到全局纯函数,几乎覆盖了声明式 UI 动效的全部基础语法。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
34
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.21 K
2.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
949
1.87 K
docsdocs
暂无描述
Markdown
906
5.84 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
537
612
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
864
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
4.29 K
1.04 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.39 K
1.48 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
550
401
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.19 K
348