在Android中,我们可以实现很多很酷的处理图片的效果。在2014年某次会议的讲演《图像的魔力》中,我介绍了其中的一部分。其中的一项技术是如何模糊图像,示例代码是使用RenderScript实现的,因为在Android中没有内置的可使用的简单的API。在这个系列中,我们将着眼于RenderScript模糊技术和JAVA实现模糊功能。我们还将进行一些基准测试,以了解每种方案的运行情况,并探讨获取最佳性能的可行方法。
让我们先从实现一个简单的可以运行的例子开始,使用RenderScript!对于没有使用过RenderScript的开发者来说,这是一个让人心生恐惧的设想,因为RenderScript真的是很难,是不是?是的,的确是。不过它也有一些事情真的很简单,而模糊图像就是其中之一。
对于不熟悉RenderScript的人来说,他是在API11中引入的,并且有一个compat库,提供RenderScript给API8及以后的版本。它本质上是一个面向图形的本地计算框架。RenderScript引擎在运行期会选择最合适的处理器(CPU或者GPU核心,多核处理器间可以分解原子操作)来执行请求的操作。本地语法基于C99,与OpenCL, CUDA, and GLSL的API相似。
如果这听起来很可怕,请稍等一下,因为我们正要简化整个过程。因为它是一个框架,允许我们创建自定义的内核来实现过滤与处理,它内置了不少可以使用的内核,其中的一个允许我们模糊图像。
后面的实例代码基于API17及更高版本开发,我已经决定不使用compat库,因为在本文写作的时候,Android Studio还不支持。此外,我们使用的模糊核心在API17后才引入,所以有最小SDK版本为17的需求。
让我们深入到一个简单的例子,这里有一个非常简单的RelativeLayout,包含了一个ImageView,上层还有个TextView:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
; html-script: false ] <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:orientation="vertical" > <ImageView android:id="@+id/image" android:src="@drawable/broadstairs" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="matrix" android:layout_centerInParent="true"/> <TextView android:id="@+id/text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello" android:layout_centerHorizontal="true" android:textColor="@android:color/white" android:layout_marginTop="300dp" android:textStyle="bold" android:textSize="48sp"/> </RelativeLayout> |
我们想要做的效果是模糊ImageView内位于TextView显示区域的图像,有效的模糊ImageView后的区域。我们最终使用的技术是拿到位于TextView区域的图像副本进行模糊,然后再将模糊后的副本设定为TextView的背景。
我刻意的设计了这种布局,使图像显示实际大小,并从显示屏的左上方开始。这样让随后的位置计算简单些,而且这里讨论的是模糊技术而不是图像定位的数学算法。尝试设定ImageView的属性android:scaleType=”center”,就会发现定位出现错乱。
此处有一个方法,它有3个参数,一个位图(已在ImageView里获得),一个视图(这是我们的TextView,但是该技术适用于任意的视图类型,所以我们将在方法中进行支持),以及一个半径用来控制模糊的程度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
; html-script: false ] private void blur(Bitmap bkg, View view, float radius) { Bitmap overlay = Bitmap.createBitmap( view.getMeasuredWidth(), view.getMeasuredHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(overlay); canvas.drawBitmap(bkg, -view.getLeft(), -view.getTop(), null); RenderScript rs = RenderScript.create(this); Allocation overlayAlloc = Allocation.createFromBitmap( rs, overlay); ScriptIntrinsicBlur blur = ScriptIntrinsicBlur.create( rs, overlayAlloc.getElement()); blur.setInput(overlayAlloc); blur.setRadius(radius); blur.forEach(overlayAlloc); overlayAlloc.copyTo(overlay); view.setBackground(new BitmapDrawable( getResources(), overlay)); rs.destroy(); } |
我们要做的第一件事是创建一个新的位图对象,用来保存想要模糊的图像区域的副本。它的大小将会是我们视图的尺寸(2-5行)。
现在我们将空的位图包装到画布中,我们可以在上面绘图(7行)。
接下来的步骤我称为”缓存剪切”,我们拷贝位图中的处于视图显示区域内的那部分图像(9-10行)。
接下来我们必须做的事情是构建一个RenderScript对象上下文,在其中我们执行模糊操作(12行)。
RenderScript是一个本地环境,它在自己的内存空间内运行,所以我们不能简单的传递位图引用,我们需要在JAVA和RenderScript内存区域进行序列化。这使用了一个分配实例来完成,这是在RenderScript内存区域中创建和引用对象的方式,为我们的位图创建一个分配会将位图的内容拷贝的分配区域中(14-15行)。
现在我们创建一个 ScriptIntrinsicBlur实例,它创建合适的RenderScript模糊脚本,并且是脚本对象的Java接口,它让我们能够控制它。(17-18行)。
脚本的输入定义了我们需要执行模糊的位图源(20行)。
现在我们设置半径来控制模糊强度(22行)。
forEach方法执行模糊操作,参数代表了结果的输出位置。我们将它写回源分配地址以减少创建的对象数量(24行)。
现在模糊完成了,但是我们必须把模糊后的图像拷贝回JAVA内存区域(26行)。
最后将模糊后的位图包装到BitmapDrawable内,并设置为视图的背景(28-29行)。
我们已经使用完RenderScript上下文,清理掉。这样做也将自动删除我们的分配(31行)。
这就是我们完整的基本模糊逻辑,但目前还不清楚什么地方和什么时候我们需要调用此方法。不幸的是关于这个问题不能简单的进行回答,所以我们将在接下来的文章中讨论。
平时我喜欢每篇文章都发布相关工作代码,但是在本例中,我们还没达到有一个可运行的完整的端到端的示例的程度。我保证在接下来的文章中进行调整。