模糊效果
模糊效果可以生动地表现出内容的层次感,能帮助用户着重关注内容。即便在模糊表面下层发生视差效果或者动态改变,也能够保持当前专题内容。
在iOS上,我们首先构造一个UIVisualEffectView得到一类模糊层:
1 2 |
UIVisualEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]; UIVisualEffectView *visualEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; |
之后添加visualEffectView到视图层,在层上可以对其下层进行动态的模糊。
Android中的表现形式
即使在Android上实现这点并非易事,但我们的确在雅虎天气应用中看到了很好的模糊效果实例。然而,根据Nicholas Pomepuy的博客文章,这个应用是通过缓存一张预渲染的背景图片来实现图片虚化的。
虽然这种方法非常有效,但其实不符合我们的需求,在500px应用中,图像通常是获得焦点的内容,而不仅仅提供背景,这说明即便是在模糊层之下,图像的变化也可能很大而且变化迅速。在我们的Android应用中就有一个恰当的例子:当用户滑动至下一页时,整排图片会以相反方向淡出,为了组成所需的模糊效果,合理地管理多个预渲染图是很困难的。
一种绘制模糊视图的方法
我们需要的效果是实时地模糊其下层的视图,最终得到的界面很简单,就像是模糊视图的一个blurred view引用。
1 |
blurringView.setBlurredView(blurredView); |
之后当blurred view改变时,不管是因为内容改变(比如展示一张新的图片)、视图的变换还是加入动画过程,我们都要刷新模糊视图。
1 |
blurringView.invalidate(); |
为了实现模糊视图, 我们需要继承View类并重写onDraw()方法来渲染模糊效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Use the blurred view’s draw() method to draw on a private canvas. mBlurredView.draw(mBlurringCanvas); // Blur the bitmap backing the private canvas into mBlurredBitmap blur(); // Draw mBlurredBitmap with transformations on the blurring view’s main canvas. canvas.save(); canvas.translate(mBlurredView.getX() - getX(), mBlurredView.getY() - getY()); canvas.scale(DOWNSAMPLE_FACTOR, DOWNSAMPLE_FACTOR); canvas.drawBitmap(mBlurredBitmap, 0, 0, null); canvas.restore(); } |
这里的关键点在于,当模糊视图重绘时,它使用blurred view的draw()方法。模糊视图保持blurred view的引用,而绘制一个私有的、以bitmap为背景的画布。
1 |
mBlurredView.draw(mBlurringCanvas); |
这种使用另一个视图的draw()方法,也适用于建立放大或者个性的UI界面。其中的内容是扩大的,而不是模糊的。
根据Nicholas Pomepuy的文章讨论出的想法,我们使用子采样和渲染脚本进行快速绘制,当初始化模糊视图的私有画布mBlurringCanvas时,子采样也随即完成了。
1 2 3 4 5 |
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); |
通过mBlurringCanvas的 建立与恰当的渲染脚本初始化,重绘时的blur()方法如下:
1 2 3 4 |
mBlurInput.copyFrom(mBitmapToBlur); mBlurScript.setInput(mBlurInput); mBlurScript.forEach(mBlurOutput); mBlurOutput.copyTo(mBlurredBitmap); |
此时mBlurredBitmap已准备好,剩下的onDraw()方法要做的就是使用适当的变换和缩放,在模糊视图自己的画布上重绘视图。
实现细节
完整实现blurring view 时,我们需要注意几个技术点。第一,我们发现缩放因子8,模糊半径15能很好地满足我们的目标,但满足你需求的参数可能是不同的。
第二,在模糊的bitmap边缘会遇到一些渲染脚本效果,我们将缩放的宽度和高度进行了圆角化,直到最近的4的倍数。
1 2 3 |
// The rounding-off here is for suppressing RenderScript artifacts at the edge. scaledWidth = scaledWidth - (scaledWidth % 4) + 4; scaledHeight = scaledHeight - (scaledHeight % 4) + 4; |
第三,为了保证更好的表现效果,我们新建两个bitmap对象,mBitmapToBlur和mBlurredBitmap,mBitmapToBlur位于私有画布mBlurringCanvas之下,mBlurredBitmap仅当blurred view的大小变化时才重新建立他们;同样地当blurred view的大小改变时,我们才创建渲染脚本对象mBlurInput和mBlurOutput。
第四,我们以PorterDuff.Mode.OVERLAY模式绘制一个白色半透明的层,它处在被模糊的的图片上层,用来处理设计需求中的淡化。
最后,由于RenderScript(渲染脚本)至少在API 17以上才可用,所以我们需要兼容Android的旧版本。糟糕的是,Nicholas Pomepuy的文章中提到的Java bitmap的模糊方案,当恰当地预渲染缓存副本时,对于实时渲染不够迅速,我们决定使用一个半透明的view来应对。(更新于2015年3月23日:通过使用RenderScript Support Library库,我的解决方法能在更低版本的API中运行。下面提及的库和Demo都更新了,非常感谢GitHub上的小伙伴panzerdev告诉我这点)
优点和缺点
我们喜欢这个视图的绘制方法,因为它可以实时模糊并且容易使用,使得blurred view的内容,也在blurring view和blurred view中间保证了灵活性,最重要的是它满足了我们的需求。
然而,这个方法确实使得blurring view与适当协同变换的blurred view保持了私有联系。相应地,模糊视图必须不能是blurred view的子类,否则会因为互相调用造成栈溢出。简单有效处理此限制的方法是要保证模糊视图与blurred view在同一级,并且Z轴上blurred view在blurring view之前。
另一点需要注意的限制是,由于与矢量图形和文本有关,我们使用默认的bitmap削减采样表现效果不是很好。
库文件和示例
你可以在我们的Android应用上看到解决方法,我们也把开源的库文件连同一个示例分享到了github上,它能够展示内容变换、动画和视图变换。