Jul
12
Liquid Glass(液态玻璃效果)实现原理续:在Android中实现液态玻璃效果GlassView
书接上回https://lrdcq.com/me/read.php/165.htm,我们探索了iOS26中液态玻璃的实现,同时在末尾留了一个尾巴,说:
- 统一渲染的操作系统,除非操作系统的renderserver支持,应该无法在性能可以接受的情况下重现这个流程的。因此旧版本iOS系统与鸿蒙系统扑街。对应,Android系统利用runtimeEffect,可以至少从流程上完整重现这个过程。
因此我们来整理一下在Android中实现液态玻璃效果需要经历哪些流程。
目前的效果如下,同时相关Demo完整代码附本文末尾:
整体流程与大致涉及的技术
如上Demo的使用代码看起来如下:
根据上次整理,液态玻璃效果的流程大致为以下几步:
1. 对内容的View进行一次离屏渲染,以对内容的形状进行获取。
- iOS中通过影子layer进行了模拟view,而在Android中,我们完全可以通过super.dispatchDraw到离屏canvas中,直接在渲染流程中进行离屏渲染。
2. sdf layer会以收集来的shape map为基础,在glass group的情况下合并多个shape map,最终得到的GPU图形渲染出sdf map。
- 也就是说在Android中,我们需要对上一步收集到的Bitmap进行SDF生成运算,得到一个新的Bitmap。这里涉及的SDF生成算法无非就是8ssedt或者jfa(Jump Flood Algorithm)算法。
- 8ssedt性能适中,适合在CPU中实现。
- JFA算法适合在GPU中实现,理论上是性能更优的算法,但是在移动端场景中,JFA的多次离屏渲染管线本身也有较高的成本,需要独立斟酌。
3. backdrop layer,在正常渲染的过程中,会离屏渲染获得它背后的layer渲染结果。
- Android中完全没有等同于backdrop能力(包括iOS和Web)的东西,如过去的讨论可能和非统一渲染架构有关。如果折中处理的话,可以类似于著名开源的BlurView的处理方式,即catch父级的内容,同样是render到一个bitmap中。
4. 当backdrop layer进入正式渲染流水线中,glassBackground shader片段会获取sdfmap,将backdrop layer最终渲染成Liquid Glass的效果。
- 此处我们即是需要输入两个Bitmap:backdrop catch结果与sdf生成结果,同时交给shader——此处不得不用上Runtime Shader了,为了高性能运算,还可以尽可能的用上RenderNode与RenderEffect来完全交给硬件渲染承接,同时也可以利用上Android自带的Blur着色器。
下文详解这个过程
Step1. 捕获GlassView的内容
这一步相当简单,只需要这样:
1. 通过在单独的offscreenCanvas中对super.dispatchDraw的调用,便将GlassView的内容绘制到独立的bitmap中了,利用传统技术没有什么疑惑。
2. 要注意的是GlassView引入了sample_scale,通过一定采样率来缩小SDF图像的尺寸,来让SDF运算更高效。实际效果上看1/2采样是相对来说均衡的,如上视频即是这个配置。
Step2. SDF生成
通过第一步得到的offscreenBitmap,生成mSDFBitmap就是一个纯粹的算法工作了,我们斟酌之后采用了8ssedt算法在主线程中直接操作bitmap的像素数据进行计算。关键代码如下:
1. 每次运算需要3个缓冲区,即记录每个像素点所谓“最近距离”的平方距离与坐标。(记录平方距离是尽可能避免开方运算,后续shader也会用到)
2. 和iOS的逻辑相同,我们只计算图像内的距离,图像外的数据和超过最大距离的数据结论上可以直接抛弃,空渲染结果就好。
3. 通过bitmap.getPixels获取像素和通过bitmap.setPixel设置像素是这个方案中唯一的输入输出部分。幸运的是setPixel性能非常好。或者是这一段代码虽然时间复杂度非常厉害,但是由于是计算密集型逻辑,在Android的AOT下性能依然非常OK。
4. 如Step1描述整个SDF图像是降采样的,同时考虑到抗锯齿的因素,这个计算逻辑得到的距离方向场,相对于iOS的SDF生成逻辑,会有严重的锯齿和错位现象。最终这个通过Step4的shader去抗抖动抹平,粗糙的效果看起来如下:
如果希望完全放到GPU中运算,则需要使用JFA算法:
- JFA应用到rendereffect流程中有3段shader,分别作为seed生成,每次一次jfa遍历,和最终的sdf生成。
- 其中核心jfa遍历其所需的Pass次数 直接取决于输入纹理的分辨率,以长边为计算,上面的demo依旧采用1/2采样长边720px,初始步长step = max(width, height) / 2即360,再以2的幂次递减,直到步长为1,则需要log2(720) = 9次pass。
- 如果以rendernode为基础,需要串接11次RenderEffect.createChainEffect看起来是:
即通过一个循环将多次RenderEffect利用createChainEffect串起来——虽然是GPU运算,但是由于高昂的单次GPU pass,动辄十几次的jfa算法的整体成本也不低,同时最终需要将渲染结果利用HardwareRenderer从GPU中取出,作为后续计算的离屏渲染缓存结果。
Step3. Backdrop获取
Backdrop获取完全模拟各个开源的BlurView即可,即:
1. 利用getViewTreeObserver().addOnPreDrawListener去见听所有View树的改变情况
2. 同时通过我们目标覆盖物backgroundTarget.isDirty()去判断我们的目标背景View是否需要重现更新
3. 一旦需要更新,直接
把目标区域draw到一个离屏缓存即可。
要注意此处和开源的blurview的不同,由于blurview的渲染结果只和Backdrop内容有关联,因此Backdrop的内容不用进行离屏缓存,可以直接上屏,使用renderNode也更合理比如:
但是对于液体玻璃来说,效果同时由sdf与backdrop决定的,存在sdf一直更新但是backdrop不变的场景,因此sdf和backdrop都不会直接上屏,而是需要render到bitmap做离屏渲染的缓存优化,因此此处用rendernode反而会比较别扭。或者说,需要结合实际场景进行优化或者切换模式可能更合理。
Step4. 液态玻璃shader运算
这一部分运算的核心是shader,和之前的iOS版本几乎没有区别,进行特殊处理的地方进行了标注:
相对于iOS版本,实际上核心逻辑的区别只有:
1. 由于如上算法的sdf图是降采样生成的,并且没有进行抗锯齿,因此有严重的条形错位。通过一个5x5的平均采样,对sdf数据进行了模糊,以保证渲染结果的平滑。
2. 其他部分其实是对这段shader能力的补充,以实现上面的边缘高光效果与更丰富的入参数。
和iOS相同, 此处也同时利用了系统提供的高斯模糊效果与当前shader串接起来,因此shader与runtimeeffect的代码看起来如下:
最终结果
最终结果即本文开始的那段视频的效果了。这个效果是对之前iOS demo的1.5:1增量复刻,虽然原理上会更接近iOS原生液体玻璃的效果,但是目前仍然有较大问题:
1. 在每一帧都需要进行sdf生成的情况下,fps在高端机都会低至15-20fps,这是不可接受的。瓶颈仍然在sdf生成上。
- 实际上iOS的使用场景中也会完全避免每一帧都需要进行sdf生成,如果能把这个过程优化到60fps作用,应该就满足了。
2. 在sdf即前景固定,仅滑动背景的情况下,fps增加到越40-50fps,作为高端机仍然无法接受。
- 这里的diff是Android不具有类似于iOS与Web的渲染引擎级的backdrop能力,手动进行背景bitmap的捕获相对低效。这个问题也是blurview等开源方案的问题,仍需要大量调优才能解决,如降采样等。
- 这个diff深入渲染架构的区别,没有可以解决的预期了。
3. 这个方案原理上和blurview一样,可以降级到renderscript解决。不过由于其中涉及的光栅化逻辑的复杂度,如果是在CPU上运行性能会更加堪忧。
- 因此液态玻璃效果在Android上的向下兼容可以认为和blurview一致。
- 这个说起来,和从来不做向下兼容SDK的苹果对比起来,谷歌还是是好人。
- 这段代码继续进行完善,同时在shader里进行基础的抗锯齿处理的话,是有希望可以达到业务可用SDK的状态的。
demo核心代码作为附件上传了:
下載檔案
- 统一渲染的操作系统,除非操作系统的renderserver支持,应该无法在性能可以接受的情况下重现这个流程的。因此旧版本iOS系统与鸿蒙系统扑街。对应,Android系统利用runtimeEffect,可以至少从流程上完整重现这个过程。
因此我们来整理一下在Android中实现液态玻璃效果需要经历哪些流程。
目前的效果如下,同时相关Demo完整代码附本文末尾:
整体流程与大致涉及的技术
如上Demo的使用代码看起来如下:
// GlassView是液态玻璃效果View,同时是一个FrameLayout
GlassView glass = new GlassView(this);
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT);
layout.setMargins(0, 300, 0, 0);
glass.setLayoutParams(layout);
glass.setPadding(10,10,10,10);
frameLayout.addView(glass);
TextView hello = new TextView(this);
hello.setTypeface(Typeface.create("sans-serif-black", Typeface.NORMAL));
hello.setText("23:59");
hello.setTextSize(128);
hello.getPaint().setFakeBoldText(true);
hello.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT));
glass.addView(hello);// 把元素放进GlassView中
glass.setCameraDistance(getResources().getDisplayMetrics().density * 10000);
ObjectAnimator shakeAnim = ObjectAnimator.ofFloat(
hello,
"rotationY",
-20f,
20f
);
shakeAnim.setDuration(5000);
shakeAnim.setRepeatCount(ValueAnimator.INFINITE);
shakeAnim.setRepeatMode(ValueAnimator.REVERSE);
shakeAnim.setInterpolator(new AccelerateDecelerateInterpolator());
shakeAnim.start(); // 做了一个动画,就是如上视频的晃来晃去的效果根据上次整理,液态玻璃效果的流程大致为以下几步:
1. 对内容的View进行一次离屏渲染,以对内容的形状进行获取。
- iOS中通过影子layer进行了模拟view,而在Android中,我们完全可以通过super.dispatchDraw到离屏canvas中,直接在渲染流程中进行离屏渲染。
2. sdf layer会以收集来的shape map为基础,在glass group的情况下合并多个shape map,最终得到的GPU图形渲染出sdf map。
- 也就是说在Android中,我们需要对上一步收集到的Bitmap进行SDF生成运算,得到一个新的Bitmap。这里涉及的SDF生成算法无非就是8ssedt或者jfa(Jump Flood Algorithm)算法。
- 8ssedt性能适中,适合在CPU中实现。
- JFA算法适合在GPU中实现,理论上是性能更优的算法,但是在移动端场景中,JFA的多次离屏渲染管线本身也有较高的成本,需要独立斟酌。
3. backdrop layer,在正常渲染的过程中,会离屏渲染获得它背后的layer渲染结果。
- Android中完全没有等同于backdrop能力(包括iOS和Web)的东西,如过去的讨论可能和非统一渲染架构有关。如果折中处理的话,可以类似于著名开源的BlurView的处理方式,即catch父级的内容,同样是render到一个bitmap中。
4. 当backdrop layer进入正式渲染流水线中,glassBackground shader片段会获取sdfmap,将backdrop layer最终渲染成Liquid Glass的效果。
- 此处我们即是需要输入两个Bitmap:backdrop catch结果与sdf生成结果,同时交给shader——此处不得不用上Runtime Shader了,为了高性能运算,还可以尽可能的用上RenderNode与RenderEffect来完全交给硬件渲染承接,同时也可以利用上Android自带的Blur着色器。
下文详解这个过程
Step1. 捕获GlassView的内容
这一步相当简单,只需要这样:
@Override
protected void dispatchDraw(@NonNull Canvas canvas) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
int sampledWidth = getWidth() / sample_scale;
int sampledHeight = getHeight() / sample_scale;
Bitmap offscreenBitmap = mBitmapPool.getBitmap(sampledWidth, sampledHeight);
Canvas offscreenCanvas = new Canvas(offscreenBitmap);
offscreenCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
offscreenCanvas.save();
offscreenCanvas.scale(1f / sample_scale, 1f / sample_scale);
super.dispatchDraw(offscreenCanvas);
offscreenCanvas.restore();
}1. 通过在单独的offscreenCanvas中对super.dispatchDraw的调用,便将GlassView的内容绘制到独立的bitmap中了,利用传统技术没有什么疑惑。
2. 要注意的是GlassView引入了sample_scale,通过一定采样率来缩小SDF图像的尺寸,来让SDF运算更高效。实际效果上看1/2采样是相对来说均衡的,如上视频即是这个配置。
Step2. SDF生成
通过第一步得到的offscreenBitmap,生成mSDFBitmap就是一个纯粹的算法工作了,我们斟酌之后采用了8ssedt算法在主线程中直接操作bitmap的像素数据进行计算。关键代码如下:
private void generateSDF(Bitmap input) {
MAX_DIST = (int) (50);
MAX_DIST_SQ = MAX_DIST * MAX_DIST;
final int w = input.getWidth();
final int h = input.getHeight();
// 从池中获取网格数据
GridData gridData = mGridPool.getGridData(w, h);
int[][] distSq = gridData.distSq;
int[][] gridDx = gridData.gridDx;
int[][] gridDy = gridData.gridDy;
// 1. 读取像素数据
int[] pixels = new int[w * h];
input.getPixels(pixels, 0, w, 0, 0, w, h);
// 2. 初始化网格 - 修正内外判断
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int alpha = (pixels[y * w + x] >> 24) & 0xFF;
if (alpha < 0.5) { // 透明像素(边界)
gridDx[y][x] = 0;
gridDy[y][x] = 0;
distSq[y][x] = 0;
} else { // 非透明像素(内部)
gridDx[y][x] = Integer.MAX_VALUE;
gridDy[y][x] = Integer.MAX_VALUE;
distSq[y][x] = Integer.MAX_VALUE;
}
}
}
// 3. 8SSEDT算法 - 第一遍(左上→右下)
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
compare(gridDx, gridDy, distSq, x, y, -1, -1); // 左上
compare(gridDx, gridDy, distSq, x, y, 0, -1); // 上
compare(gridDx, gridDy, distSq, x, y, 1, -1); // 右上
compare(gridDx, gridDy, distSq, x, y, -1, 0); // 左
}
}
// 4. 8SSEDT算法 - 第二遍(右下→左上)
for (int y = h - 1; y >= 0; y--) {
for (int x = w - 1; x >= 0; x--) {
compare(gridDx, gridDy, distSq, x, y, 1, 0); // 右
compare(gridDx, gridDy, distSq, x, y, -1, 1); // 左下
compare(gridDx, gridDy, distSq, x, y, 0, 1); // 下
compare(gridDx, gridDy, distSq, x, y, 1, 1); // 右下
}
}
// 5. 获取或创建SDF Bitmap
mSDFBitmap = mBitmapPool.getBitmap(w, h);
// 6. 填充SDF Bitmap - 保证所有内部像素都有有效向量
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int distSqVal = distSq[y][x];
float r; // 距离值
// 计算归一化距离
if (distSqVal == 0) {
r = 1.0f; // 边界点
} else if (distSqVal >= MAX_DIST_SQ) {
r = 0.0f; // 超出最大距离
} else {
r = 1.0f - (float) Math.sqrt(distSqVal) / MAX_DIST;
}
// 计算向量 - 确保所有内部点都有有效向量
float g = 0.5f;
float b = 0.5f;
if (distSqVal > 0 && distSqVal < Integer.MAX_VALUE) {
int dx = gridDx[y][x];
int dy = gridDy[y][x];
// 所有非零向量都进行计算(包括短向量)
if (dx != 0 || dy != 0) {
double len = Math.sqrt(dx * dx + dy * dy);
g = (float) (dx / len + 1) * 0.5f;
b = (float) (dy / len + 1) * 0.5f;
}
}
int color = Color.argb(255,
(int) (r * 255), // R通道 = 距离
(int) (g * 255), // G通道 = X方向
(int) (b * 255) // B通道 = Y方向
);
mSDFBitmap.setPixel(x, y, color);
}
}
}
private void compare(int[][] gridDx, int[][] gridDy, int[][] distSq,
int x, int y, int dx, int dy) {
int w = gridDx[0].length;
int h = gridDx.length;
int nx = x + dx;
int ny = y + dy;
if (nx < 0 || ny < 0 || nx >= w || ny >= h) return;
if (distSq[ny][nx] == Integer.MAX_VALUE) return;
int newDx = gridDx[ny][nx] + dx;
int newDy = gridDy[ny][nx] + dy;
int newDistSq = newDx * newDx + newDy * newDy;
if (newDistSq < distSq[y][x]) {
distSq[y][x] = newDistSq;
gridDx[y][x] = newDx;
gridDy[y][x] = newDy;
}
}1. 每次运算需要3个缓冲区,即记录每个像素点所谓“最近距离”的平方距离与坐标。(记录平方距离是尽可能避免开方运算,后续shader也会用到)
2. 和iOS的逻辑相同,我们只计算图像内的距离,图像外的数据和超过最大距离的数据结论上可以直接抛弃,空渲染结果就好。
3. 通过bitmap.getPixels获取像素和通过bitmap.setPixel设置像素是这个方案中唯一的输入输出部分。幸运的是setPixel性能非常好。或者是这一段代码虽然时间复杂度非常厉害,但是由于是计算密集型逻辑,在Android的AOT下性能依然非常OK。
4. 如Step1描述整个SDF图像是降采样的,同时考虑到抗锯齿的因素,这个计算逻辑得到的距离方向场,相对于iOS的SDF生成逻辑,会有严重的锯齿和错位现象。最终这个通过Step4的shader去抗抖动抹平,粗糙的效果看起来如下:
如果希望完全放到GPU中运算,则需要使用JFA算法:
- JFA应用到rendereffect流程中有3段shader,分别作为seed生成,每次一次jfa遍历,和最终的sdf生成。
- 其中核心jfa遍历其所需的Pass次数 直接取决于输入纹理的分辨率,以长边为计算,上面的demo依旧采用1/2采样长边720px,初始步长step = max(width, height) / 2即360,再以2的幂次递减,直到步长为1,则需要log2(720) = 9次pass。
- 如果以rendernode为基础,需要串接11次RenderEffect.createChainEffect看起来是:
int step = 360;
RenderEffect seed = RenderEffect.createRuntimeShaderEffect(seedShader, "input");
RenderEffect jfa = seed;
while (step > 0) {
RuntimeShader jfaShaderC = jfaShader.clone();
jfaShaderC.setFloatUniform("step", step);
jfa = RenderEffect.createChainEffect(
RenderEffect.createRuntimeShaderEffect(jfaShaderC, "source"),
jfa
);
step /= 2;
}
sdfShader.setFloatUniform("maxDistance", maxDistance);
RenderEffect sdf = RenderEffect.createChainEffect(
RenderEffect.createRuntimeShaderEffect(sdfShader, "source"),
jfa
);
mRenderNode.setRenderEffect(sdf);即通过一个循环将多次RenderEffect利用createChainEffect串起来——虽然是GPU运算,但是由于高昂的单次GPU pass,动辄十几次的jfa算法的整体成本也不低,同时最终需要将渲染结果利用HardwareRenderer从GPU中取出,作为后续计算的离屏渲染缓存结果。
Step3. Backdrop获取
Backdrop获取完全模拟各个开源的BlurView即可,即:
1. 利用getViewTreeObserver().addOnPreDrawListener去见听所有View树的改变情况
2. 同时通过我们目标覆盖物backgroundTarget.isDirty()去判断我们的目标背景View是否需要重现更新
3. 一旦需要更新,直接
Canvas bgCanvas = new Canvas(backgroundBitmap);
bgCanvas.translate(-tmpRect.left, -tmpRect.top);
backgroundTarget.draw(bgCanvas);把目标区域draw到一个离屏缓存即可。
要注意此处和开源的blurview的不同,由于blurview的渲染结果只和Backdrop内容有关联,因此Backdrop的内容不用进行离屏缓存,可以直接上屏,使用renderNode也更合理比如:
RecordingCanvas recordingCanvas = blurNode.beginRecording();
recordingCanvas.drawRenderNode(backgroundTarget.renderNode);
applyBlur();
blurNode.endRecording();但是对于液体玻璃来说,效果同时由sdf与backdrop决定的,存在sdf一直更新但是backdrop不变的场景,因此sdf和backdrop都不会直接上屏,而是需要render到bitmap做离屏渲染的缓存优化,因此此处用rendernode反而会比较别扭。或者说,需要结合实际场景进行优化或者切换模式可能更合理。
Step4. 液态玻璃shader运算
这一部分运算的核心是shader,和之前的iOS版本几乎没有区别,进行特殊处理的地方进行了标注:
uniform shader sdf;
uniform shader source;
uniform shader background;
uniform float2 size;
uniform float sample;
uniform float scale;
uniform float ref_height;
uniform float ref_length;
uniform float ref_border_width;
uniform float ref_exposure;
half4 getColorWithOffset(float2 coord, float2 offset) {
half4 color = source.eval(coord + offset);
half3 rgb = color.rgb * ref_exposure;
return half4(rgb, color.a);
}
float _linear_map(float x, float y, float a, float b, float tsetnumber) {
float ratio = (tsetnumber - x) / (y - x);
return a + ratio * (b - a);
}
half4 main(float2 fragCoord) {
float2 sdfCoord = fragCoord / sample;
half4 sdf_c = sdf.eval(sdfCoord);
// 对sdf图像进行了模糊处理
float2 gb = float2(0.0);
float count = 0;
for (int x = -2; x <= 2; x++) {
for (int y = -2; y <= 2; y++) {
half4 sdf_n = sdf.eval(sdfCoord + float2(x, y));
if (sdf_n.r <= 0.99999) {
count++;
gb.x += sdf_n.g;
gb.y += sdf_n.b;
}
}
}
sdf_c.g = gb.x / count;
sdf_c.b = gb.y / count;
if (sdf_c.r >= 0.99999) {
return background.eval(fragCoord);
}
float dis = (1.0 - sdf_c.r) * 50.0 * scale;
float r_height = ref_height;
float r_length = ref_length;
if (dis < r_height) {
float offsetVal = _linear_map(r_height, 0.0, r_height, r_height - r_length, dis);
float offset = dis - offsetVal;
float2 normal = float2(sdf_c.gb) * 2.0 - 1.0;
float2 offset_normal = normal * offset;
half4 result = getColorWithOffset(fragCoord, offset_normal);
// 应用法线偏移,实现边缘效果
if (dis <= ref_border_width) {
float rate = 0.0;
if (normal.x * normal.y > 0.0) {
float angle = atan(abs(normal.y), abs(normal.x)) / 3.1415926 * 2;
if (angle < 0.5) {
rate = angle * 2.0;
} else {
rate = 2.0 - angle * 2.0;
}
}
result = result * (1.0 + rate * 0.8);
}
return result;
} else {
return getColorWithOffset(fragCoord, float2(0.0));
}
}相对于iOS版本,实际上核心逻辑的区别只有:
1. 由于如上算法的sdf图是降采样生成的,并且没有进行抗锯齿,因此有严重的条形错位。通过一个5x5的平均采样,对sdf数据进行了模糊,以保证渲染结果的平滑。
2. 其他部分其实是对这段shader能力的补充,以实现上面的边缘高光效果与更丰富的入参数。
和iOS相同, 此处也同时利用了系统提供的高斯模糊效果与当前shader串接起来,因此shader与runtimeeffect的代码看起来如下:
Shader background = new BitmapShader(backgroundBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Shader sdf = new BitmapShader(mSDFBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
rs.setInputShader("sdf", sdf);
rs.setInputShader("background", background);
rs.setFloatUniform("size", getWidth(), getHeight());
rs.setFloatUniform("sample", (float) sample_scale);
rs.setFloatUniform("scale", mScale);
rs.setFloatUniform("ref_height", ref_height);
rs.setFloatUniform("ref_length", ref_length);
rs.setFloatUniform("ref_border_width", ref_border_width);
rs.setFloatUniform("ref_exposure", ref_exposure);
mRenderNode.setPosition(0, 0, backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
Canvas renderCanvas = mRenderNode.beginRecording();
renderCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
renderCanvas.drawBitmap(backgroundBitmap, 0, 0, new Paint());
mRenderNode.endRecording();
mRenderNode.setRenderEffect(
RenderEffect.createChainEffect(
RenderEffect.createRuntimeShaderEffect(rs, "source"),
RenderEffect.createBlurEffect(blur_radius, blur_radius,Shader.TileMode.CLAMP)
)
);
canvas.drawRenderNode(mRenderNode);最终结果
最终结果即本文开始的那段视频的效果了。这个效果是对之前iOS demo的1.5:1增量复刻,虽然原理上会更接近iOS原生液体玻璃的效果,但是目前仍然有较大问题:
1. 在每一帧都需要进行sdf生成的情况下,fps在高端机都会低至15-20fps,这是不可接受的。瓶颈仍然在sdf生成上。
- 实际上iOS的使用场景中也会完全避免每一帧都需要进行sdf生成,如果能把这个过程优化到60fps作用,应该就满足了。
2. 在sdf即前景固定,仅滑动背景的情况下,fps增加到越40-50fps,作为高端机仍然无法接受。
- 这里的diff是Android不具有类似于iOS与Web的渲染引擎级的backdrop能力,手动进行背景bitmap的捕获相对低效。这个问题也是blurview等开源方案的问题,仍需要大量调优才能解决,如降采样等。
- 这个diff深入渲染架构的区别,没有可以解决的预期了。
3. 这个方案原理上和blurview一样,可以降级到renderscript解决。不过由于其中涉及的光栅化逻辑的复杂度,如果是在CPU上运行性能会更加堪忧。
- 因此液态玻璃效果在Android上的向下兼容可以认为和blurview一致。
- 这个说起来,和从来不做向下兼容SDK的苹果对比起来,谷歌还是是好人。
- 这段代码继续进行完善,同时在shader里进行基础的抗锯齿处理的话,是有希望可以达到业务可用SDK的状态的。
demo核心代码作为附件上传了:
下載檔案
从0逆向探索iOS26 Liqu
渝公网安备 