DroidGlass: 跨平台的液态玻璃
Liquid Glass 是苹果的一种能折射其下方内容的材料。靠近边缘时,背景会像厚透镜一样被拉向中心,边缘会显示柔和的彩虹色,边缘有一条发丝般的高光线,触摸时玻璃会膨胀、向手指方向拉伸并弹回。普通的毛玻璃效果仅会在白色雾状层下模糊背景,这使得它看起来廉价且有磨砂感。
品味的主要规则:**玻璃是透明的。**如果你无法阅读其背后的内容,那是 iOS 7 的磨砂玻璃,而不是 Liquid Glass。仅在文本位于玻璃上时才添加模糊和表面层。
诚实地告诉用户局限性在哪里。真实的材质仅存在于 iOS、iPadOS 和 macOS 26 及更高版本上;其他所有平台都是模仿。在 Android 上,Backdrop 库提供了完整的模拟(透镜效果需要 Android 13)。在 Web 上,折射效果仅在 Chromium 内核(Chrome、Edge、三星浏览器、Android WebView、Electron)上有效。Safari、Firefox 以及 iOS 上的所有浏览器会获得带有相同高光、阴影和弹性效果的轻度模糊,但不会有弯曲效果。
工作流程
- 识别平台。
- iOS/macOS 26 上的 SwiftUI、UIKit、AppKit、WidgetKit:使用系统 API,参见
references/apple.md。不要模仿任何东西。 - Jetpack Compose 或 Compose Multiplatform:
references/android-compose.md。不要从头开始编写着色器,使用库。 - Web、Electron、Android WebView、Capacitor、Cordova、Windows/Android 上的 Tauri:使用
assets/中的引擎,参见references/web.md。在 WebView 和 Electron 中折射始终有效。 - React Native、Flutter:没有系统等效方案。对于 React Native,建议使用 WebView 层或基于 Backdrop 的原生模块;对于 Flutter,
BackdropFilter作为后备方案,使用来自references/math-and-motion.md公式的FragmentProgram。自行检查当前可用的包,此技能未列出它们。
- iOS/macOS 26 上的 SwiftUI、UIKit、AppKit、WidgetKit:使用系统 API,参见
- **选择控制层的 1 到 3 个元素:**标签栏、浮动按钮、顶部栏、工作表、菜单、开关或触摸时的滑块拇指。不要使内容变为玻璃。
- **从下表选择一个配方。**不要发明参数:这些值来自 Kyant 的目录并已在屏幕上验证。
- **检查玻璃下的背景。**玻璃仅在丰富内容上可见:照片、颜色、图表、滚动时的文本。在纯白页面上的透明玻璃几乎会消失:在其后方放置颜色、添加调暗层或移除该处的玻璃。
- 根据正确的参考构建并使用末尾的检查清单目视检查。未提供静态、按下和拖动状态截图前,不要交付。
苹果的规则
来自人机界面指南、WWDC25“Meet Liquid Glass”和流行的社区技能。违反其中任何一条都会立刻暴露是仿制品。
- 仅限导航和控制层漂浮在内容之上。列表、行、卡片和页面背景不是玻璃。例外:触摸时,开关和滑块拇指变为玻璃。
- **玻璃上无玻璃。**玻璃上的元素通过填充和不透明度进行区分。来自 Backdrop 的例外:玻璃工作表上的滑块拇指会折射工作表(
exportedBackdrop):一个透镜叠加在另一个透镜上,而不是两个磨砂板。 - **少量使用。**仅用于最重要的控件。
- **玻璃是无色的。**颜色和亮度来自背景:浅色背景上为浅色,深色背景上为深色,高光由背景着色。没有自身的白色或灰色表面。
- **两种模式,不要混合。**清晰(按钮、拇指、放大镜、与按钮组合的标签栏):模糊 0 到 4,无表面。常规(含有长文本的工作表、对话框):模糊 8 到 16;仅在无表面时文本不可读的情况下,添加 40% 到 60% 的表面。
- 仅为主操作添加色调。
- **形状。**胶囊形:半径等于高度的一半。同心圆角:内圆角半径等于容器半径减去内边距。
- **越大越厚:**大元素获得更宽的弯曲边缘和更深的折射。
- 切勿让内容一半位于玻璃下方。
- **液态在运动中。**按下时,玻璃会增长几个像素并以阻尼方式向手指方向倾斜;拇指会膨胀 1.5 倍并以速度拉伸成水滴状。一切都在弹性运动上运行,没有线性过渡。
配方(来自 Kyant 目录的值)
| 元素 | 效果 | 高光和阴影 | 触摸 |
|---|---|---|---|
| 按钮,48 dp 胶囊形 | 活力,模糊 2,透镜(12, 24) | 默认,默认阴影 | 发光;高度增加 4 dp;偏移 min × tanh(0.05 × offset / min);向手指方向拉伸最多 4 dp |
| 有色调的按钮 | 同上 + 色调 | 同上 | 同上 |
| 带表面的按钮 | 同上 + 白色 30% | 同上 | 同上 |
| 开关拇指 40×24,轨道 64×28 | 静态:白色 100% 和模糊 8;按下 p:模糊 8(1−p),透镜(5p, 10p, 色散),白色 (1−p) | 环境,宽度 /1.5,alpha p;阴影半径 4,5%;内阴影半径 4p,alpha p | 缩放至 1.5;轨道通过透镜可见,缩小至 0.75 |
| 滑块拇指 40×24,轨道 6 dp | 像开关,透镜(10p, 14p) | 像开关 | 缩放至 1.5;点击轨道会跳转至该点 |
| 标签栏 64 dp | 活力,模糊 8,透镜(24, 24),表面 #FAFAFA 40%(深色 #121212 40%) | 默认 | 按下时整个面板宽度增加 16 dp(标签一起),并在手指后倾斜最多 4 dp |
隐藏行副本 56 dp (tabsBackdrop) | 相同面板但使用强调色,alpha(0):活力,模糊 8,透镜(24p, 24p),面板表面 | 默认带 alpha p,InteractiveHighlight | 按下时图标和标签放大 ×1.2(LocalLiquidBottomTabScale 仅在此行上设置) |
| 标签栏拇指 56 dp | 静态:灰色 10%;按下:透镜(10p, 14p, 色散) 覆盖在背景和行副本之上(rememberCombinedBackdrop(backdrop, tabsBackdrop)) | 默认,阴影和内阴影 8p 带 alpha p | 缩放 78/56;在水滴内部有一个“栏中之栏”:其自身项为强调色并放大,两侧的相邻项可见 |
| 对话框,半径 48 | 亮度 0.2(浅色),饱和度 1.5,模糊 16(深色 8),透镜(24, 48, 深度) | 纯色 | 背景变暗 #29293A 23%(深色 #121212 56%) |
| 控制中心磁贴 | 活力,透镜(24, 48, 深度) | 默认,角度来自设备倾斜,衰减 2 | 进入:偏移 48 dp 和 alpha |
| 放大镜 128×96 | 透镜(8, 24, 深度, 色散),背景缩放 1.5x | 内阴影 16 | 拖动 |
弹性运动、手势以及每个平台共享的数学原理都在 references/math-and-motion.md 中。
一分钟内搞定 Web 和 Android WebView
将 assets/ 中的三个文件原封不动复制到项目中,并使用选项进行配置。不要重写引擎。
<link rel="stylesheet" href="liquid-glass.css">
<button id="buy">购买</button>
<div id="wifi" aria-label="Wi-Fi"></div>
<div id="volume" style="width: 280px"></div>
<nav id="tabs" style="position: fixed; left: 16px; right: 16px; bottom: 16px"></nav>
<script src="liquid-glass.js"></script>
<script src="liquid-ui.js"></script>
<script>
LiquidUI.button(document.getElementById('buy'), { tint: '#0088FF', onClick: () => {} });
LiquidUI.toggle(document.getElementById('wifi'), { checked: true, onChange: (on) => {} });
LiquidUI.slider(document.getElementById('volume'), { value: 0.4, onInput: (v) => {}, label: '音量' });
const tabs = LiquidUI.tabs(document.getElementById('tabs'), {
items: [{ label: '首页', icon: '<svg>…</svg>' }, { label: '搜索', icon: '<svg>…</svg>' }],
index: 0, onChange: (i) => {},
});
// 融合:一个栏和一个相邻的按钮融合成一个水滴;相邻的弹性按钮会自动融合
// LiquidUI.blob([tabs.panel, fab], { blur: 3.5 }); LiquidUI.autoMerge(document.body); 在屏幕变化后调用 LiquidUI.sweep()
// 为您的自定义标记提供裸玻璃效果
const g = LiquidGlass.attach(document.getElementById('panel'), { radius: 28, blur: 8, refractionHeight: 24, refractionAmount: 24, surface: 'rgb(250 250 250 / .4)' });
// g.set({ lens: 0..1, blur, surface, highlightAlpha, angle, shadowAlpha, innerShadow, press: { p, x, y }, tx, ty, sx, sy, sleep })
</script>
| 文件 | 说明 |
|---|---|
assets/liquid-glass.js | 玻璃引擎:折射映射、色散、高光、阴影、融合组 (LiquidGlass) |
assets/liquid-glass.css | 基础样式、无障碍回退、组件样式 |
assets/liquid-ui.js | 弹性运动、手势和组件:按钮、开关、滑块、标签、放大镜、blob、autoMerge (LiquidUI) |
assets/demo.html | 所有组件在单个页面中;打开它查看正确效果是什么样的 |
在修改任何内容或将其接入应用之前,请阅读 references/web.md:它列出了几乎会破坏所有实现的 Chromium 关键点(背景根元素、position: fixed、touch-action、body 背景)以及所有选项。当某些效果看起来不对时,请参考 references/troubleshooting.md;当出现卡顿时,请参考 references/performance.md。
Web 上绝不能出错的五件事:
- 背景必须是一个真实的元素。设置在
body上并传播到根元素的背景不会被backdrop-filter看到。 - 切勿在玻璃的祖先元素上对
opacity、filter、mask或clip-path进行动画(例如屏幕淡入):这会使其成为背景根元素,导致玻璃失去背景并变暗。使用transform移动屏幕,并使用顶部的遮罩层来显示它们。 - 不要在玻璃元素上设置自己的
transform;将基础偏移写入--lg-base。 - 弹性按钮需要
touch-action: none(默认值)。使用pan-y时,浏览器会劫持拖动并发送pointercancel。 - 在 Android WebView 中禁用长按和触觉反馈:
setOnLongClickListener(v -> true)和setHapticFeedbackEnabled(false)。
可访问性和可读性
- Web 引擎遵循
prefers-reduced-transparency: reduce(关闭玻璃效果,使用几乎不透明的表面;Chrome 和 Edge 119+,Safari 或 Firefox 尚不支持)、prefers-contrast: more(实心表面和边框)以及prefers-reduced-motion(弹性运动变为瞬时)。 - 组件获得角色和键盘支持:按钮(Enter, Space)、
switch(Enter, Space)、slider(方向键)、tablist(左、右方向键)。使用aria-label或aria-labelledby为它们添加标签。 - 在 Android 上设置
semantics { role = Role.Switch }和标签,如 Kyant 的目录中所示;在旧版 Android 上检查onDrawSurface颜色在没有效果的情况下是否可读。 - 检查最亮和最繁忙内容上的文本对比度,这些内容可能最终位于面板下方:正文文本需要 4.5:1。如果失败,添加模糊和表面,或移动内容。文本阴影无法解决此问题。
不要做
- 按钮上使用白色半透明表面加
blur(10px):那是磨砂玻璃,不是 Liquid Glass。仅在标签栏、工作表和要求文本的对话框上可以使用密度。 - 使用玻璃内容卡片和玻璃页面背景。
- 在大面板上使用色散:昂贵且俗气。
- 使用粗白边框代替发丝般的高光线。
- 每个按钮使用相同的色调。
- 使用线性
transition代替弹性运动来处理按下和拖动。 - 在 Web 上对玻璃的
width和height进行动画:映射会在每一帧重建。请对 scale 进行动画。 - 在结果被多个步骤读取的过程中使用繁重的滤镜步骤(模糊、置换、卷积、形态):Chrome 会为每个分支计算一次,导致手机冻结。
- 为了“美观”而自行绘制玻璃表面。只有包含文本的密集面板才需要表面。
- 在 Backdrop 可用时编写自己的 Compose 着色器。
- 在苹果平台之外使用 SF Symbols 和 Apple 徽标:其许可禁止这样做。请使用开源的图标集。
- 将 Web 或 Android 的结果称为“官方 Liquid Glass”。它只是风格上的模仿。
交付前检查清单
在 2 倍缩放比例下查看截图,亲自检查:
- **静态时叠加在网格或文本上:**线条通过透明玻璃可见,在边缘处向中心拉拢,高光很细。在白色背景上,玻璃通过边缘阴影和投影与页面区分;在深色背景上,通过光照侧的一条亮线区分。
- **按下:**按钮增长并向手指方向倾斜;拇指膨胀且透明,轨道透过它可见,彩虹柔和,没有彩色空洞。
- **拖动:**拇指以速度拉伸,仅在到达后才收缩;标签栏倾斜并回位。标签栏水滴在边缘标签和靠近屏幕底部边缘处:其自身项在内,相邻项在边缘,没有黑色区域、条纹或镜像碎片。
- **拉动:**按钮跟随手指在橡皮筋上弹动并回弹;一个栏被拉向相邻按钮时会融合到其中,桥梁随着它们靠近而增长,没有接缝或双重边缘;在静态和简单按下时它们保持分离;正常点击有效,远距离拉动后释放的按钮不会触发。
- **叠加在照片和白色背景上:**玻璃下的色彩丰富,文本可读。
- 深色主题。
- **回退方案:**模拟引擎(Safari 用户代理和空的
navigator.userAgentData)并检查元素是否没有变成黑洞,标签栏拇指下的图标是否没有模糊。 - **缩放和旋转:**映射重建,边缘没有阶梯步骤。
- **屏幕过渡:**捕获屏幕进入时的连续几帧;玻璃按钮绝不会比静态时更暗,合并层在动画后精确地位于其按钮下方。
- 无控制台错误且无卡顿:在连续点击后页面响应速度如常。一个动画循环,没有多余的
requestAnimationFrame。 - 手机屏幕上不超过 3 个大玻璃元素,无玻璃上玻璃,无玻璃内容。
如果某个检查项失败,先修复它,然后再展示给用户。
来源
- Kyant0/AndroidLiquidGlass、Backdrop 库和目录(Apache-2.0):透镜、色散、高光和阴影公式、组件配方、弹性运动和手势常量。Web 引擎是该方案的移植版本。
- Apple:人机界面指南“Materials”、“Adopting Liquid Glass”、WWDC25“Meet Liquid Glass”。
- kube.io,“Liquid Glass in the Browser”:通过
backdrop-filter内的feDisplacementMap实现折射。 - iyinchao/liquid-glass-studio(MIT):通过
smin融合形状、Snell 折射、无色玻璃。
……