在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:
- <?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="<a href="http://www.jobbole.com/members/android/" rel="nofollow">@android</a>: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,但是该技术适用于任意的视图类型,所以我们将在方法中进行支持),以及一个半径用来控制模糊的程度。
- 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行)。
这就是我们完整的基本模糊逻辑,但目前还不清楚什么地方和什么时候我们需要调用此方法。不幸的是关于这个问题不能简单的进行回答,所以我们将在接下来的文章中讨论。
平时我喜欢每篇文章都发布相关工作代码,但是在本例中,我们还没达到有一个可运行的完整的端到端的示例的程度。我保证在接下来的文章中进行调整。
via:
我们介绍了使用RenderScript使另一个视图范围内的图片部分模糊。但是实际上,我们并没有深入地调用这个方法来研究图像模糊行为。原因是我们需要在性能方面进行仔细考虑,这篇文章我们会进行更进一步地的探索。
调用这个方法最直白的方法是父布局的onDraw()。有经验的开发者读到这里可能会开始摇头,我们应该保持onDraw方法的实现尽可能有效。以前的文章中的代码包括创建对象、位图操作和切换到renderScript上下文。其中,OnDraw会降低帧速率。你可以不相信我的做法,但是可以通过测量并证明它是有效的。在后面的系列中,我们就会这样做。
如果布局是静态的(即我们的布局不包含任何动画),在布局时就不会改变待模糊的位置和范围。只有在布局改变时执行该操作才有意义,但前提是布局的所有视图大小和位置根据布局变化测量和计算过。这里有一个非常实用的技巧,可以注册一个OnGlobalLayoutListener监听函数。当布局发生改变的时候会调用onGlobalLayout()方法。当我们收到布局已经改变的通知时,注册的OnPreDrawListener监听函数的onPreDraw()方法会被调用每当执行onDraw方法。我们要做的第一件事情就是取消注册onPreDraw()方法,这样只有在布局改变的时候才会被调用,而不是每次onDraw方法触发时都调用。下面可以执行模糊方法,从这个方法的返回值很重要,使用它可以让我们放弃onDraw操作,重复之前的布局。这对在回调函数中修改布局非常有帮助,但是这里不需要这么做。所以返回true,继续绘制。
我们的Activity代码如下:
- public class MainActivity extends Activity {
- private ImageView mImage;
- private TextView mText;
-
- private OnPreDrawListener mPreDrawListener =
- new OnPreDrawListener() {
-
- @Override
- public boolean onPreDraw() {
- ViewTreeObserver observer = mText.getViewTreeObserver();
- if(observer != null) {
- observer.removeOnPreDrawListener(this);
- }
- Drawable drawable = mImage.getDrawable();
- if (drawable != null &&
- drawable instanceof BitmapDrawable) {
- Bitmap bitmap =
- ((BitmapDrawable) drawable).getBitmap();
- if (bitmap != null) {
- blur(bitmap, mText, 25);
- }
- }
- return true;
- }
- };
-
- private OnGlobalLayoutListener mLayoutListener =
- new OnGlobalLayoutListener() {
-
- @Override
- public void onGlobalLayout() {
- ViewTreeObserver observer = mText.getViewTreeObserver();
- if(observer != null) {
- observer.addOnPreDrawListener(
- mPreDrawListener);
- }
- }
- };
-
- /**
- * Called when the activity is first created.
- */
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- mImage = (ImageView) findViewById(R.id.image);
- mText = (TextView)findViewById(R.id.text);
- if (mImage != null && mText != null) {
- ViewTreeObserver observer =
- mText.getViewTreeObserver();
- if (observer != null) {
- observer.addOnGlobalLayoutListener(
- mLayoutListener);
- }
- }
- }
-
- private void blur(Bitmap bkg, View view, float radius) {
- ....
- }
- }
在父布局覆盖onDraw方法的优点是,可以在布局层次上任意附加OnPreDrawListener方法。因此需要自定义一个布局,这个布局是标准布局的子类,这样就可以覆盖onDraw方法。使用predrawlistener意味着可以非常容易在任意布局中添加。
最后,模糊图片实现如下:
在下一篇文章中,我会更深入地回答为什么要避免在onDraw中执行模糊操作,并且还会介绍有用的性能测量工具。
这篇文章的代码在这里。
via:
原文链接: stylingandroid 翻译: 伯乐在线- fhdis