桌面卡片想要磨砂玻璃,而 Glance 什么模糊接口都没给。最后那张玻璃是我自己一层一层糊出来的位图。这篇记的是这条pipeline上几个绕不过去的数字和限制。
为什么只能自己画
先把「没有」说清楚,都是拆过东西确认的,不是照网上抄的结论:
androidx.glance:glance-appwidget:1.1.1的classes.jar里,androidx.glance下只有BackgroundKt.background(ColorProvider / Int / Long)和VisibilityModifier,没有任何 Blur / Alpha Modifierandroid.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 三个色 + hintsgetBuiltInDrawable()—— 那是出厂默认壁纸,用户换了就不对,不能当当前壁纸用
于是兜底那条路(也就是主路)是色场:白打底,三个取色点各压 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
还没有评论,来抢沙发