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的背景色吧。


像我们看到的一样,结果很好并且花了57ms。由于Android的框架需致使不超过16ms(60fps),在UI线程虚化的时间段中,使我们的帧频率下降到17fps。显然这是不允许的,因此我们需要卸载这个,利用asyncTask或者其他类似的技术。


同时值得注意的是ScriptIntrinsicBlur只有在API 17时才可用。但是你可以利用renderscript依赖包兼容低版本的API。


但是,大量的人仍然不得不支持那些没有这样功能的老版本的API。在这让我们寻找我们可以找到的。



FastBlur


由于虚化的过程只不过是操作像素,显著的解决方案是试着尽量手动虚化。很幸运,有大量的java例子可以实现虚化效果。需要做的唯一事情是找到相对快速的实现方案。


感谢在SO上得博客,我找到了快速虚化的实现方式。让我们看看吧。

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);
    overlay = FastBlur.doBlur(overlay, (int)radius, true);
    view.setBackground(new BitmapDrawable(getResources(), overlay));
    statusText.setText(System.currentTimeMillis() - startMs + "ms");
}


像我们看到的,虚化的效果也差不多。

因此,利用FastBlur的优点是消除renderscript的依赖。


糟糕!它竟然消耗了大量的时间。进行虚化花费了147ms。这远远不是最慢得SW模糊算法。我甚至不想尝试高斯模糊了。。。


Going beyond


现在让我们想想有什么更好的办法。虚化的过程它本身是一个不稳定的像素。妮知道的还有什么是关于不稳定像素的?对!Downscaling!


假设我们试着先缩减bitmap,进行模糊,然后重新扩大它 会怎么样?我尝试实现这个技术。


我只用了faseblur方法进行描述,对于renderscript是一样的。全部的代码可以查看我的github。

private void blur(Bitmap bkg, View view) {
    long startMs = System.currentTimeMillis();
    float scaleFactor = 1;
    float radius = 20;
    if (downScale.isChecked()) {
        scaleFactor = 8;
        radius = 2;
    }

    Bitmap overlay = Bitmap.createBitmap((int) (view.getMeasuredWidth()/scaleFactor),
            (int) (view.getMeasuredHeight()/scaleFactor), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(overlay);
    canvas.translate(-view.getLeft()/scaleFactor, -view.getTop()/scaleFactor);
    canvas.scale(1 / scaleFactor, 1 / scaleFactor);
    Paint paint = new Paint();
    paint.setFlags(Paint.FILTER_BITMAP_FLAG);
    canvas.drawBitmap(bkg, 0, 0, paint);

    overlay = FastBlur.doBlur(overlay, (int)radius, true);
    view.setBackground(new BitmapDrawable(getResources(), overlay));
    statusText.setText(System.currentTimeMillis() - startMs + "ms");
}

让我们仔细检查一下代码:


1.scaleFactor表示我们想要缩减模式的层级。在我的例子中,我将缩减bitmap到原来大小的1/8 。当然因为我的bitmap将会通过缩减/扩大模式进行模糊,我并不需要比较大的半径来进行模糊算法。我决定半径减为2.


2.现在我需要创建bitmap。这个bitmap比我最终需要的背景小8倍。


3.同时请注意我提供了paint的FILTER_BITMAP_FLAG。这个方法中,在缩放时将得到双线过滤应用到我的bitmap上。它可以更加平滑的模糊。


4.就像之前,使用虚化。在这个例子中,image较小,模糊半径较低,所以模糊的速度就会很快。


5.设置模糊的image作为背景色。它将重新自动扩大。


fastblur模糊的速度要远远超过renderscript。那是因为我们没有浪费时间在复制bitmap到Allocation。

利用这个简单的处理,我设法比较了fast blur机制和renderscript的依赖。

警告!请注意 FastBlur需要用大量的额外内存。(它复制了整个图片到临时缓冲区),即使它对于较小的bitmap完美运行,我不推荐利用它来进行虚化整个屏幕,因为这样在低版本设备上很容易发生内存溢出异常(OutOfMemoryException)。选择最好的决定。


源代码地址:https://github.com/paveldudka/blurring 


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值