<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>KMP on Trevor's Blog</title><link>http://mings.in/tags/kmp/</link><description>Recent content in KMP on Trevor's Blog</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Fri, 06 Nov 2026 08:05:00 +0800</lastBuildDate><atom:link href="http://mings.in/tags/kmp/index.xml" rel="self" type="application/rss+xml"/><item><title>Compose 桌面跑通了 Liquid Glass</title><link>http://mings.in/posts/liquid-glass/</link><pubDate>Fri, 06 Nov 2026 08:05:00 +0800</pubDate><guid>http://mings.in/posts/liquid-glass/</guid><description>苹果的 Liquid Glass，安卓一直没有官方答案。我把 Kyant0/Backdrop 装进一个 Compose Multiplatform 桌面工程，在 macOS 上真的跑通了：跨端适配只写了 280 行，因为 AGSL 是 SkSL 的子集。</description><content:encoded><![CDATA[<p>去年苹果在 iOS 26 上放出 Liquid Glass 的时候，安卓这边最热闹的讨论只有一句：我们什么时候能有。</p>
<p>官方的答案是没有。Jetpack Compose 到今天也没有内置这种材质，你想在界面上放一块会折射、会随背景变色的玻璃，要么自己写着色器，要么找一个库。</p>
<p>AndroidLiquidGlass 就是这样一个库，作者 Kyant0，4,509 星。名字里带 Android，但它在描述里的定位是 Compose Multiplatform，主分支直接叫 kmp。</p>
<p>我一直好奇一件事：这种重度依赖平台图形能力的视觉效果，跨端到底能还原几成。</p>
<p>安卓上画玻璃靠的是 AGSL 着色器，桌面靠 Skia。两条完全不同的图形栈，一个库想同时喂饱它们，通常的结局是凑合能用，效果打折。</p>
<p>所以我做了件笨事：在本机建了个最小的 Compose Multiplatform 桌面工程，把库加进去，在 macOS 上让玻璃真的画出来，然后一个效果一个效果拆开看。</p>
<p>结论放前面：能跑，效果也不是凑合。整库 2,550 行，跨端适配只写了 280 行，原因在源码里，后面会说到。</p>
<h2 id="先认清它是谁">先认清它是谁</h2>
<p>仓库叫 <code>Kyant0/AndroidLiquidGlass</code>，4,509 星、364 个 fork，Apache-2.0。名字里带 Android，但描述写的是 Compose Multiplatform Liquid Glass effect，主分支叫 kmp。</p>
<p>库现在的正式名字是 Backdrop，Maven 坐标 <code>io.github.kyant0:backdrop</code>，最新版 2.0.1，八月二十六号发的。</p>
<p>README 里有句话值得抄下来：这个库不包含任何高层组件，组件要你自己搭。它给的是效果原语，不是一行调用就出来的现成按钮。</p>
<p>体量也小：桌面端 jar 136 KB，连它依赖的 shapes 库 46 KB 一起，总共不到两百 KB。</p>
<p><img loading="lazy" src="/images/liquid-glass/fig1.png"></p>
<h2 id="最小工程就三步">最小工程就三步</h2>
<p>新建一个 Gradle 工程，装 Kotlin 2.4.10、Compose Multiplatform 1.12.0 两个插件，然后加依赖：</p>
<p><code>implementation(&quot;io.github.kyant0:backdrop:2.0.1&quot;)</code></p>
<p>代码就一个模式，记住「谁被采样」和「谁去采样」必须配对：</p>
<p>后台的内容用 <code>Modifier.layerBackdrop(backdrop)</code> 注册成采样源，前面的面板用 <code>Modifier.drawBackdrop(backdrop, shape, effects)</code> 去取。</p>
<p>我用一个画了四个色块加一层细网格的 Canvas 当背景，前面放一个圆角二十六的面板，效果里依次调 vibrancy、blur、lens。窗口一开，玻璃就出来了。</p>
<p><img loading="lazy" src="/images/liquid-glass/fig2.png"></p>
<h2 id="它长什么样">它长什么样</h2>
<p>我把四种效果拆开各跑一格，同一张背景，同一个面板，只改效果栈。</p>
<p>第一格只给形状，什么都不加，你会发现面板里透出的网格是清晰的。这一点很关键：这说明它不是预先烘焙一张模糊图，而是每帧真的去采样后面的内容。</p>
<p>第二格加 8dp 模糊，网格糊成一片。第三格叠 vibrancy，颜色明显更艳。第四格再加 lens，面板边缘开始出现那圈像水滴一样的折射。</p>
<p><img loading="lazy" src="/images/liquid-glass/fig3.png"></p>
<p>vibrancy 到底是什么，官方文档写得很实在：就是把饱和度乘 1.5，等价于 <code>colorControls(saturation = 1.5f)</code>。</p>
<h2 id="它其实给了五种-backdrop">它其实给了五种 backdrop</h2>
<p>文档里 Backdrops 那一页列了五种，用途分得很细。</p>
<p><code>rememberLayerBackdrop</code> 最常用，把某个 Composable 的内容录成采样源，必须配 <code>layerBackdrop</code>。</p>
<p><code>rememberCombinedBackdrop</code> 把多个 backdrop 合成一个，用来做标签栏和滑块这种跨区域采样。<code>rememberCanvasBackdrop</code> 不依赖布局坐标，可以往一块空 backdrop 上画自己的东西。<code>emptyBackdrop</code> 就是什么都不画。</p>
<p>还有一个 <code>rememberBackdrop</code>，让你用自定义命令直接画。</p>
<p>分这么细的原因是不这么分就做不出高级效果。比如底部标签栏的玻璃要能采到「整页内容」，而不是只采自己后面那一小块，那就得先合成。</p>
<p>这部分是我觉得它比同类库扎实的地方：不是给一个 API 让你套，是把采样这件事的各种边界都拆出来给你。</p>
<h2 id="一个必须自己补的坑">一个必须自己补的坑</h2>
<p>我第一版在浅色背景上跑，面板几乎是白的，字全看不清。</p>
<p>原因不怪库：玻璃本来就应该透，浅色背景上它自然也是浅色，白字自然就没了。iOS 上系统会替你压一层材质，这库不管这层。</p>
<p>解法是自己加。<code>drawBackdrop</code> 有个 <code>onDrawSurface</code> 参数，我在里面画了一层 30% 的黑：</p>
<p><code>onDrawSurface = { { drawRect(Color(0x30000000)) } }</code></p>
<p>加完两边都正常了，浅色深色各一张对比图。</p>
<p><img loading="lazy" src="/images/liquid-glass/fig4.png"></p>
<h2 id="为什么桌面能跑">为什么桌面能跑</h2>
<p>这是我最想弄清的一点。库名叫 Android，实现里也真的用了 <code>android.graphics.RuntimeShader</code>，那桌面凭什么能画？</p>
<p>翻源码就明白了。整个库分三块：commonMain 2,270 行写所有逻辑，androidMain 146 行、skikoMain 134 行负责两端落地。</p>
<p>着色器是 AGSL 字符串。安卓侧把它塞进系统的 RuntimeShader；Skia 侧直接喂给 <code>RuntimeEffect.makeForShader</code>，因为 AGSL 本来就是 SkSL 的子集。</p>
<p>也就是说 Android 上那套玻璃着色器，原样搬到 Skia 上就能编译。</p>
<p>桌面、iOS、wasm 走的都是这条 skiko 路径。</p>
<p>两端的支持判断也写在源码里：安卓要 <code>SDK_INT &gt;= 31</code> 才有 RenderEffect，要 <code>&gt;= 33</code> 才有 RuntimeShader；Skia 那边两个函数直接返回 true，不设门槛。</p>
<h2 id="还有两个不那么顺的地方">还有两个不那么顺的地方</h2>
<p>第一个在文档里，但容易被忽略：lens 效果要求 <code>shape</code> 必须是 <code>CornerBasedShape</code>。我用 <code>RoundedCornerShape</code> 所以没事，换成自定义 Shape 就得自己处理。</p>
<p>第二个在源码里，文档没提：Skia 实现不支持整型数组 uniform。<code>setIntUniform(name, values: IntArray)</code> 在桌面端会直接抛 <code>UnsupportedOperationException</code>，安卓侧是好的。这类差异平时碰不到，一旦你写自定义着色器就会撞上。</p>
<p>顺带说一句文档的标记方式。官方 API 页写着「效果本质上是 RenderEffect，Android 12 起生效，涉及 RuntimeShader 的要 Android 13」。这句在桌面语境下不成立，我在 macOS 上用 lens 一点问题没有。它那句是站在安卓角度说的，别当成平台限制。</p>
<h2 id="性能和我踩的边界">性能和我踩的边界</h2>
<p>我这台 M 系列 Mac 上，两块玻璃一起动，帧率稳定在 60，正好顶到刷新率。加到九块玻璃同屏、每块都带动态 lens，掉到 58.7 帧。</p>
<p>也就是说这个效果的开销，在这个量级上还没成为瓶颈。</p>
<p>编译方面，第一次带依赖解析跑了 25 秒，之后靠 Gradle 缓存基本一秒内。</p>
<p>这里也把截图方式说清楚：macOS 没给屏幕录制权限，系统级截屏只能拍到壁纸。所以我改用 SkiaLayer 自带的 <code>screenshot()</code>，直接拿窗口画面写 PNG，像素是准的。</p>
<p><img loading="lazy" src="/images/liquid-glass/fig5.png"></p>
<h2 id="社区情况">社区情况</h2>
<p>4,509 星不是白来的，这个效果确实抓人。但节奏摆在那：提交停在八月二十六号，文档站的更新时间写着「5 个月前」。</p>
<p>40 个 open issue，官方 FAQ 里第一条是「为什么效果不生效」，答案是大概率忘了加 <code>layerBackdrop</code>。第二条更直接，是一个安卓端的 SIGSEGV 崩溃，在 RenderThread 里炸，要照着教程去改。</p>
<p>这库是好东西，但它是个人项目，不是官方组件。指望它像 Compose Foundation 那样稳定，会失望。</p>
<h2 id="我的判断">我的判断</h2>
<p>想在 Compose 桌面上做点 macOS 味的玻璃面板，这个库现在就能用，成本两百 KB 加二十行代码。</p>
<p>但别把它当成品组件用。它给的是效果，交互、可访问性、深色浅色的适配全是你自己的活。苹果那套东西好看，一半功劳在系统替你处理了这些边角。</p>
<p>我打算把它用在自己的小工具上试试。真正想验证的是那件事：一个 Android 原生库，靠 AGSL 和 SkSL 的这层血缘，在桌面上跑得跟原生一样。</p>
<p>你的跨平台项目里，会为了一个视觉效果去引一个个人维护的库吗？</p>
]]></content:encoded></item></channel></rss>