Android 虚化的高级技巧
今天我们试着深入研究一些模糊技巧献给安卓开发者们。我阅读了大量的文章和so帖子中对于它的不同的描述方法,所以我想总结一下我学到的东西。
为什么?
如今越来越多的开发者试着为他们的自定义控件增加各种类型的模糊背景。看看比较出色的Muzei app和Yahoo app。我真的比较喜欢他们的设计。
写这篇文章的灵感来自here(by Mark Allison)的一套博客。所以我博客的第一部分将会很像Marks的博客。但我会尽力走的更远。
基本上将在今天完成下图的效果。
预备知识
让我描述下我要工作的内容。我需要一个主体activity作为不同的fragment的在viewpager中寄主。每一个fragment代表一个模糊技巧。
layout_main.xml
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/pager" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.paveldudka.MainActivity" />fragment_layout.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/picture" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/picture" android:scaleType="centerCrop" /> <TextView android:id="@+id/text" android:gravity="center_horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="My super text" android:textColor="@android:color/white" android:layout_gravity="center_vertical" android:textStyle="bold" android:textSize="48sp" /> <LinearLayout android:id="@+id/controls" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#7f000000" android:orientation="vertical" android:layout_gravity="bottom"/> </FrameLayout>就像你看到的一样,这是imageview和textview居中显示,并且添加一个调试布局(@+id/controls).我将利用它显示性能测定并添加一些优化。
一般的模糊技巧就是这样:
1.剪切藏在textview背后的背景图的一部分。
2.把它虚化。
3.设置需要虚化的部分作为textview的背景。
Renderscript
对于像“Android怎么实现模糊效果”这种问题,最流行的答案是Renderscript。它是一个很强大的图表工具。我在下面内容将不会解释它的工作原理,它超出了这篇博客的范围。
public class RSBlurFragment extends Fragment { private ImageView image; private TextView text; private TextView statusText; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_layout, container, false); image = (ImageView) view.findViewById(R.id.picture); text = (TextView) view.findViewById(R.id.text); statusText = addStatusText((ViewGroup) view.findViewById(R.id.controls)); applyBlur(); return view; } private void applyBlur() { image.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { image.getViewTreeObserver().removeOnPreDrawListener(this); image.buildDrawingCache(); Bitmap bmp = image.getDrawingCache(); blur(bmp, text); return true; } }); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private void blur(Bitmap bkg, View view) { long startMs = System.currentTimeMillis(); float radius = 20; Bitmap overlay = Bitmap.createBitmap((int) (view.getMeasuredWidth()), (int) (view.getMeasuredHeight()), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(overlay); canvas.translate(-view.getLeft(), -view.getTop()); canvas.drawBitmap(bkg, 0, 0, null); RenderScript rs = RenderScript.create(getActivity()); 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(); statusText.setText(System.currentTimeMillis() - startMs + "ms"); } @Override public String toString() { return "RenderScript"; } private TextView addStatusText(ViewGroup container) { TextView result = new TextView(getActivity()); result.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); result.setTextColor(0xFFFFFFFF); container.addView(result); return result; } }1.当fragment被创建,进行初始化布局,增加textview作为调试面板(我将利用它显示虚化的执行效率)并且将image虚化。
2.在applyBlur()方法中注册了onPreDrawListener().我这么做是因为 此刻applyBlur()被调用,但是布局什么都没有,因此,这时需要虚化的东西为空。我需要等到计算出加载的布局文件并展示出来以后。
3.在回调onPreDraw()方法时,我通常会改变生成的false,返回true值。如果返回false,框架将会跳过绘图的步骤。所以我会返回true。
4.然后我移除回调因为再也不需要监听绘图前的事件。
5.现在我想得到在imageview之外的Bitmap。创建了drawing cache并调用getDrawingCache()重新获取。
6.最后终于要虚化了。让我们看具体步骤
我在这想说我意识到我的代码并没有覆盖这几个比较重要的片刻:
1)当布局改变的时候不会二次虚化。鉴于此,你需要注册onGlobalLayoutListener并在每次布局变化的时候重新虚化。
2)在主线程中虚化。显然妮不会在产品中这么做,但为了简单起见,我现在就照这样做了。
所以,让我们回到blur()方法中;
1.首先,我创建了一个空的bitmap并存放复制的部分背景图,稍后我会虚化这个bitmap并设置成textview的背景色。
2.通过bitmap的备份创建一个canvas对象。
3.调用canvas将textview添加到父布局中。
4.绘制imageview的一部分作为bitmap。
5.此时此刻,我有一个bitmap,大小和textview一样大,包括imageview的部分图片并作为textview的背景色。
6.创建一个Renderscript实例。
7.复制我的bitmap到Renderscript-friendly的数据片段。
8.创建一个Renderscript虚化的实例。
9.设置内容,半径和虚化的应用控件。
10.复制结果,回到bitmap。
11.好了!现在我们已经虚化了bitmap。看看textview的背景色吧。