Skia 在很早就支持了这个能力,在 Android 12 中,谷歌在上层封装了RenderEffect类,第一次将其开放给上层调用[4]。
void
SkCanvas::DrawDeviceWithFilter(SkBaseDevice* src,
const
SkImageFilter* filter, SkBaseDevice* dst,
const
SkIPoint& dstOrigin,
const
SkMatrix& ctm) {
// 省略代码
……
// 截取当前已经绘制的内容
auto
special = src->snapSpecial(backdropBounds);
if
(!special) {
return
;
}
// 省略代码
…
SkIPoint offset;
// 使用指定的filter进行处理
special = as_IFB(filter)->filterImage(ctx).imageAndOffset(&offset);
if
(special) {
offset += layerInputBounds.topLeft;
SkMatrix dstCTM = toRoot;
dstCTM.postTranslate(-dstOrigin.x, -dstOrigin.y);
dstCTM.preTranslate(offset.fX, offset.fY);
// 将处理结果进行绘制
dst->drawSpecial(special.get, dstCTM, sampling, p);
}
// 省略代码
……
}
结合注释与代码得知,有两个组合可以实现背景模糊效果:
组合一: 将 backdrop 设置为对应的filter,比如 SkBlurImageFilter 即可,将 saveLayerFlags 设为0。
组合二: backdrop 仍然为null,但 saveLayerFlags 设为 kInitWithPrevious_SaveLayerFlag ,此时这个新建的layer会将之前layer的内容复制一份。然后使用 paint->setImageFilter ,将 filter 设置到 paint 中。
区别在于,前者会将整个 Canvas 的内容都进行处理,然后 clip 到相应区域;而后者只会截取指定区域的内容,另外也不会立即处理,而是选择在新 layer 上屏的时刻统一处理。
Flutter 使用的是第一个组合。笔者两个方案都进行了尝试,本文与Flutter保持一致,讨论第一种组合。
思路解析
看完 Flutter,我们再来看看 Android 里的
saveLayer
逻辑,经过一些中转,它最终调用到了这里
// File: frameworks/base/libs/hwui/SkiaCanvas.cpp
int
SkiaCanvas::saveLayer(
float
left,
float
top,
float
right,
float
bottom,
const
SkPaint* paint, SaveFlags::Flags flags) {
const
SkRect bounds = SkRect::MakeLTRB(left, top, right, bottom);
// 这里只用到了 bounds, paint, layerFlags 三个参数
const
SkCanvas::
SaveLayerRec
rec
(&bounds, paint, layerFlags(flags))
;
return
mCanvas->saveLayer(rec);
}
可以看到,Android 直接忽略了后面两个参数,并没有提供任何暴露途径。
那我们的思路也就很简单了:
新增 Canvas.saveLayer 的overload方法,增加 backdropFilter 参数。在内部转为对 SaveLayerRec 的后两个参数的设置。
暴露对 SkImageFilter 对象的创建方法。如果是Android 12环境,则可以跳过这一步,使用 RenderEffect.getNativeInstance 即可。
由于是对 Canvas 的调用,简单的办法是封装成 Drawable,供 View 设置为background使用。
<?xml version="1.0" encoding="utf-8"?>
<
coolx.graphics.drawable.BackdropBlurDrawable
xmlns:android
=
"http://schemas.android.com/apk/res/android"
xmlns:app
=
"http://schemas.android.com/apk/res-auto"
app:blurRadius
=
"30dp"
app:saturation
=
"1.8"
app:fallbackColor
=
"#AAFFFFFF"
>
<
shape
android:shape
=
"rectangle"
>
<
solid
android:color
=
"#BFEFEFEF"
/>
</
shape
>
</
coolx.graphics.drawable.BackdropBlurDrawable
>
实机效果演示
优点
使用简单、效果好 。将 SkImageFilter 进行组合,可以轻松实现透亮的毛玻璃效果。图例叠加了模糊和饱和度的修改,效果非常接近 iOS。
兼容性高 。模糊控件可以随意动画、clip。
使用场景受限制 。由于是调用 Canvas 接口,所以封装为 Drawable 设置为 View 的背景更适合使用。此时也会受到与方案二相同的限制:虽然限制少一些,可以正常 clip圆角,但 当 View 设置 alpha 的时候,模糊仍会失效 。
需要修改系统源码 。
这个方案我提交到了AOSP[5],谷歌工程师给了两个反馈:
性能差(very, very slow)。
在 alpha 的时候会失效。
第一条反馈与实际表现不符合,关于性能是否符合要求,需要进一步的调查和实验。
但第二条确实如此,所以还需要继续找寻新的解法。
方案四:修改libhwui,增加模糊计算
View 的 alpha 发生改变,其实是设置的
RenderNode.setAlpha
方法。
方案二与方案三,由于都使用了
Canvas
的接口,所以无论是重写
View.onDraw
方法,还是封装
Drawable
,这个调用指令都在该
View
的
RenderNode
内部。这样当 alpha 变化时,就无法获取到背景内容。
除了 alpha 外,这次也打算将所有的边界场景一次考虑清楚:View 的 transform、动画、clip 等。目前能想到的方案,是让背景模糊逻辑脱离
RenderNode
。
参照前文,
RenderNode
的真正绘制,是在
RenderNodeDrawable
中,我们可以新定义一个
BackdropFilterDrawable
类型,与其平级。
将
BackdropFilterDrawable
的绘制顺序,提前到
RenderNodeDrawable
之前即可。
BackdropFilterDrawable
类的关键逻辑如下:
void BackdropFilterDrawable::onDraw(SkCanvas* canvas) {
// 对后面内容进行截图(并不会创建新的buffer),此截图为Canvas完整截图。
auto backdropImage = canvas->getSurface->makeImageSnapshot;
// 从target RenderNode那里,同步properties,无论它是否在做动画、缩放、是否有clip等,都进行同步。计算结果保存到 mImageSubset 里,这是我们上层RenderNode真正的可见区域。
if
(!prepareToDraw(canvas, properties, backdropImage->width, backdropImage->height)) {
// 当返回false的时候,说明不可见,则我们也跳过绘制。
return
;
}
auto imageSubset = mImageSubset.roundOut;
// 将截图里的上层区域进行filter处理。
backdropImage =
backdropImage->makeWithFilter(canvas->recordingContext, backdropFilter, imageSubset,
imageSubset, &mOutSubset, &mOutOffset);
// 将filter结果进行绘制。
canvas->drawImageRect(backdropImage, SkRect::Make(mOutSubset), mDstBounds,
SkSamplingOptions(SkFilterMode::kLinear), &mPaint,
SkCanvas::kStrict_SrcRectConstraint);
}
优点
与方案三相同,优势在于性能和效果。
使用简单、效果好 。将 SkImageFilter 进行组合,可以轻松实现透亮的毛玻璃效果。图例叠加了模糊和饱和度的修改,效果非常接近 iOS。
兼容性高 。模糊控件可以随意动画、clip、变换 alpha。
使用场景受限制 。虽然解决了方案二的 alpha 问题,但如果该控件的父布局设置了 alpha,它仍然无法拿到父布局以外的背景内容。这算是个小小的遗憾。
需要修改系统源码 。对系统源码的改动,比方案三多不少。
这个方案我也提交到了AOSP[6],目前状态为待 Review。感兴趣的可以编译看下效果。
方案对比
我们在酷派COOL 20s 5G上,将四种方案进行横向对比。
这台机器配置为天玑700、6GB内存、128GB存储、1080p 90Hz的屏幕。
使用perfetto抓取的trace,来衡量和计算每种方案在
Choreographer.doFrame
和
RenderThread
分别消耗的时间。
在perfetto里可以直观地看到平均耗时
抓取无模糊效果的耗时,作为基准指标。分别为:doFrame 1.588ms, RenderThread: 3.485ms。
最终对比结果如下:
方案一综合开销最高,后面三个方案的
doFrame
耗时,与基准耗时的差异在误差范围内,几乎没有引入额外的计算。
综合来看,
方案四
各方面表现都很优秀,酷派在自研的COOLOS里,已经有多处采用了它。
后记
经过这样一番调研,笔者的有很多感悟和提升,其中感触最深的是:如果对一块领域感兴趣,但网络和社区没有更好解法时,就自己读源码吧。
在这个能力的调研过程中,有非常多有意思的技术问题,每一项都值得深入去探讨和研究。
比如:
为什么有时候模糊边缘会闪烁?
模糊运算原理是什么?模糊计算本身有没有性能优化空间?
想使用酷派调研的方案,有没有办法不修改源码来用到它们?
由于篇幅限制,本
文不再展开,有机会可以开些续文,详细讲讲。
感兴趣的读者,欢迎评论区跟我们一起讨论!
参考链接
Window Blurs | Android Open Source Project (https://source.android.com/devices/tech/display/window-blurs)
Glide v4 : Hardware Bitmaps (https://bumptech.github.io/glide/doc/hardwarebitmaps.html#why-should-we-use-hardware-bitmaps)
glCopyTexSubImage2D | khronos.org (https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glCopyTexSubImage2D.xml)
RenderEffect | Android Developers (https://developer.android.com/reference/android/graphics/RenderEffect)
方案三 | Android Code Review
方案四 | Android Code Review
为了防止失联,欢迎关注我防备的小号
微信改了推送机制,真爱请星标本公号
👇
返回搜狐,查看更多