[译] 在 Android 下进行实时模糊渲染

6,773
原文链接: github.com

模糊渲染

模糊渲染能生动地表达内容间的层次感。当专注于当前特定内容的时候,它允许用户维持相对的上下文,即使模糊层下面的内容发生了视差移动或者动态变化。

在IOS开发中,我们首先可以通过构造UIVisualEffectView获得这种模糊效果:

UIVisualEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
UIVisualEffectView *visualEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];

接着我们可以添加visualEffectView到视图层中,那么在它之下的内容都会动态渲染模糊效果。

在Android中的现状

虽然在Android中并没有直接的方法实现模糊渲染,但我们依然能见到些十分优秀的例子比如Yahoo Weather应用,见Nicholas Pomepuy的博文,然而,它是通过缓存一张预先渲染模糊的背景图片实现的。

虽然这种方法挺有效果,但并不是我们想要的。在500px社区,图片并不是用作背景而是焦点内容,这意味着图片可以随意改变甚至迅速改变,即使它们被覆盖在模糊层之下。我们的Android应用就是个十分典型的例子。比如,当用户滑到下一页时,图片会向反方向移动并淡出,通过适当地维护多个预先渲染模糊的图片是很难满足这种需求的。

查看图片

通过自定义View的OnDraw方法

我们的需求是希望能实现一个模糊视图,它能实时动态地模糊渲染在它之下的视图。我们最终想要的代码最好能尽量简单例如直接让模糊视图拥有一份被模糊视图的引用:

    blurringView.setBlurredView(blurredView);

然后当被模糊视图改变时 - 不管是内容的改变(如显示张新的图片)、视图的移动、或者是视图动画,我们都需要刷新模糊视图:

    blurringView.invalidate();

为了实现模糊视图,我们需要继承View类然后重写onDraw()方法来渲染模糊效果:

    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // 让被模糊视图的draw()方法在私有的画布上绘制
    mBlurredView.draw(mBlurringCanvas);

    // 模糊私有画布的位图并传递给mBlurredBitmap
    blur();

    // 经过转换后将mBlurredBitmap绘制在模糊视图的默认画布上
    canvas.save();
    canvas.translate(mBlurredView.getX() - getX(), mBlurredView.getY() - getY());
    canvas.scale(DOWNSAMPLE_FACTOR, DOWNSAMPLE_FACTOR);
    canvas.drawBitmap(mBlurredBitmap, 0, 0, null);
    canvas.restore();
    }

这里的关键是当模糊视图重绘的时候,我们会通过对被模糊视图的引用来调用它的draw方法,同时它会在我们私有的画布上绘画(译者注:对该画布的操作最终会作用到我们私有的位图上):

    mBlurredView.draw(mBlurringCanvas);

(通过这种途径访问其它的视图的draw方法十分有参考价值,我们也可以实现一个放大镜或者用来标注的视图,相对于模糊渲染,放大镜或者标注的区域更需要放大。)

下面的想法在Nicholas Pomepuy的博文中有谈到,我们结合二次抽样与RenderScript进行快速处理。在我们完成模糊视图的私有画布mBlurringCanvas的初始化后二次抽样也设置完成:

    int scaledWidth = mBlurredView.getWidth() / DOWNSAMPLE_FACTOR;
    int scaledHeight = mBlurredView.getHeight() / DOWNSAMPLE_FACTOR;

    mBitmapToBlur = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
    mBlurringCanvas = new Canvas(mBitmapToBlur);

通过了上面的设置后再适当地初始化RenderScript。那么上文onDraw()调用的blur()方法就简单多了:

    mBlurInput.copyFrom(mBitmapToBlur);
    mBlurScript.setInput(mBlurInput);
    mBlurScript.forEach(mBlurOutput);
    mBlurOutput.copyTo(mBlurredBitmap);

注意此时mBlurredBitmap已经渲染好了,余下的工作是onDraw()方法对它适当的移动和缩放后绘制到模糊视图默认画布中。

实现细节

对于完全的实现,我们需要留心多个技术细节。首先,我们意识到,8个单位的缩放采样以及15个单位的模糊半径就能很好地呈现我们想要的效果。当然,或许对你来说,别的参数才能满足你的需求。

其次,在模糊位图的边缘处我们遇到了一些RenderScript的历史遗留问题,为了应对这个问题,我们对宽度和高度缩放到近似4倍。

    // The rounding-off here is for suppressing RenderScript artifacts at the edge.
    scaledWidth = scaledWidth - (scaledWidth % 4) + 4;
    scaledHeight = scaledHeight - (scaledHeight % 4) + 4;

第三,我们为了更好地保证性能,需要创建两张位图分别是mBitmapToBlur做为私有画布mBlurringCanvas的底图和mBlurredBitmap,并会在被模糊视图的大小改变时重新创建它们。同时,我们也需要重新创建RenderScript的Allocation对象也就是mBlurInputmBlurOutput

第四,为了设计的明亮程度考虑,当最上面的被模糊视图拥有属性PorterDuff.Mode.OVERLAY时我们也可以绘制一个统一白色半透明层。

最后,由于RenderScript仅在API版本17及以上有效,我们在较低级版本也应该有个比较优雅的降级方案。可不幸的是,正如Nicholas Pomepuy的博文中说的那样,通过Java来实现图片模糊渲染速度上达不到实时渲染的需求。最后我们只能决定使用个有较高透明度的半透明视图做为降级方案。

优缺点

我们欣赏这个视图的绘制策略因为它能做到实时模糊同时十分简单易用。它无需知道被模糊视图的内容,同时在模糊和被模糊视图的关系之间有很大的灵活性。当然,最重要的是他很好地满足了我们的需求。

然而,这种策略需要模糊视图通过适当的坐标转换来掌握被模糊视图的位置。关键是模糊视图不能是被模糊视图的子视图否则你将会收到堆栈溢出错误提示因为它们在互相调用对方的绘制方法。一个简单但又十分有效摆脱这种限制的方法是保证模糊视图是被模糊视图的姊妹视图并通过z-order来变换它们的层次关系。

还有个注意点是对于矢量图和文本,默认的位图采样并不太有效。

类库和演示

你可以在我们的Android应用中看到完全的解决方案。同时我们也在GitHub上推出了一个轻量级的开源类库,里面有个演示应用来展示如何在内容发生改变和视图动画时使用该类库。