粥粥
学了个习

桌面小组件的磨砂玻璃,是我自己一层层糊出来的

阅读 6 约 5 分钟 暂无评论

桌面卡片想要磨砂玻璃,而 Glance 什么模糊接口都没给。最后那张玻璃是我自己一层一层糊出来的位图。这篇记的是这条pipeline上几个绕不过去的数字和限制。

为什么只能自己画

先把「没有」说清楚,都是拆过东西确认的,不是照网上抄的结论:

  • androidx.glance:glance-appwidget:1.1.1 的 classes.jar 里,androidx.glance 下只有 BackgroundKt.background(ColorProvider / Int / Long) 和 VisibilityModifier,没有任何 Blur / Alpha Modifier
  • android.widget.RemoteViews 在 android-35 和 android-36.1 的 android.jar 里,javap | grep -i blur 是空的——一个 blur 方法都没有
  • 桌面进程肯不肯给卡片后面糊一层,不由 App 说了算

也就是说这条路上没有任何接口能借。但 ImageProvider 有一个 Bitmap 重载(javap androidx.glance.ImageKt 能看到 ImageProvider(Bitmap)),所以「高斯模糊」只能变成:App 自己糊一张图,把它当背景递进去。

三个数字:120、3、360

糊一张图的代价主要是采样边长。我的常量是:

private const val SampleSide = 120   // 采样一档的边长
private const val BlurPasses = 3     // 盒式模糊跑几遍
private const val RenderSide = 360   // 出图短边

流程是:取到源图 → 缩到 120 一档 → boxBlurArgb 三次 → 双线性放大回 360 → 在 Canvas 上叠色调/高光/描边 → 抠圆角。

为什么是三次盒式:一次盒式核是个方框,糊完边缘会留一条明显的方块边;三次迭代卷积起来近似高斯(中心极限定理那一套),糊出来的过渡是圆的。

为什么出图只有 360:反过来说——不能拿卡片实际像素尺寸出图。一张 656×817 的 ARGB_8888 就是 2.1 MB,而 RemoteViews 整棵树的 Parcel 预算只有 ~1 MB,塞进去直接在 update 时炸。360 这一档糊过之后放大看不出错,尺寸又稳在预算里。

糊好的图用 LruCache<String, Bitmap>(8) 按 wallpaperId 缓存:壁纸换了 key 变、重画;卡片挪了格子不跟,因为小组件根本不知道自己摆在屏幕哪一格。

拿不到壁纸像素,是权限的事

读壁纸像素在 Android 17 的模拟器上实测被拦:WallpaperManager.drawable / peekDrawable() 一律抛

Permission android.permission.READ_EXTERNAL_STORAGE denied

别信「读壁纸不需要权限」那种老文档。为了这个去要存储权限不划算(会被商店和数据安全表追问),所以不要权限也能拿到的东西只有两样:

  • getWallpaperColors(FLAG_SYSTEM) —— primary / secondary / tertiary 三个色 + hints
  • getBuiltInDrawable() —— 那是出厂默认壁纸,用户换了就不对,不能当当前壁纸用

于是兜底那条路(也就是主路)是色场:白打底,三个取色点各压 70% / 50% 画径向色块,然后走同一条模糊 pipeline。这样两条路的输出形状一致,上层代码不用分支。

动态壁纸(wallpaperInfo != null)单独处理:那时只是 drawable 给的是应用的预览图,不能当壁纸像素用,但 getWallpaperColors 照样给得出色 —— 所以是「跳过像素、只走取色」,不是整条退回纯半透明 layer-list。

圆角和大圆角的代价

GlanceAppWidget.cornerRadius(Dp) 只在 Android 12+ 生效,minSdk 26 用不了。要圆角只能让背景本身是一张 shape:22dp 圆角 + 1dp <stroke>,颜色走 @color/… + values-night/colors.xml 跟系统深色(桌面进程解析资源,ColorProvider 本来也是按系统配置走的)。

<shape> 不能同时 gradient + solid,所以那张 layer-list 是分层的:上亮下暗的半透明色调(91% → 79% alpha)、顶部一道薄光、一圈描边当玻璃边,每层自己写 corners。

顺带一条容易踩的:background(ImageProvider(...)) 的默认 ContentScale 是 FillBounds(= fitXY),不是 Crop(看 background-l7F5y5Q$default 的字节码里写死 ContentScale.getFillBounds,AAR 里带着 glance_image_fill_bounds.xml / glance_image_crop.xml / glance_image_fit.xml 三套布局)。所以自糊的位图不会被裁边,但比例对不上时圆角会被轻微拉扁。

至于「圆角外面到底该不该垫一张壁纸快照」——那个我踩了三个版本才弄明白,写在另一篇里:https://www.zhouzhou.work/index.php/archives/1090/(DST_IN 在位图 canvas 上是个彻底的空操作)。

玻璃这件事在 App 界面那一侧被撤了

只有桌面卡片留着玻璃。App 里顶栏底栏在 v1.21.1 做过一版仿 iOS 26 的液态玻璃(Haze 模糊 + 四层边缘光 + 内高光内阴影 + 悬浮胶囊),做完自己看着就不对,整个撤了:dev.chrisbanes.haze 依赖、GlassBar.kt、glassBg / glassHighlight / glassInnerShadow 三个令牌一起删掉。现在是通栏实底 + 上缘一道发丝线,不悬浮、不模糊、不投影。

分层这件事靠实底和发丝线就够了,拟物光泽不是必需。桌面卡片之所以还能留玻璃,是因为它必须跟着壁纸走色——那是另一种需求,不是同一种审美。

量「到底透了多少」别拿卡片平均值当数

最后一条方法论。卡片里带字的行会把像素均值拉低,看着像透其实全是字。正确取法:

1. 取一条没有文字的空白带(比如中间那段弹簧区域)

2. 取同一行、卡片左右两侧露出的壁纸

3. 反解:alpha = (卡片 − 壁纸) / (255 − 壁纸)

实测桌面把 79% 的白叠在 (15,15,24) 的深色壁纸上读出来是 (209,209,211),反解 ≈ 82%,和写进去的 alpha 对得上。

像素是用 ffmpeg 数的(PIL 装不上):

ffmpeg -y -v error -i card.png -f rawvideo -pix_fmt rgb24 card.rgb
python3 -c "d=open('card.rgb','rb').read(); W,H=1080,2424; print(d[(900*W+520)*3:(900*W+540)*3])"

注意 PPM/头那类切分坑:PPM 头里有三个换行,切错了整列会偏 4 字节。

粥粥

种一棵树,等它长大

转载请注明出处

不知道看什么?

随机去往一篇

评论 0

添加新评论

还没有评论,来抢沙发