赞
踩
前段时间项目中新增了需求,要在一个动态的背景(比如聊天或者弹幕)上显示一个View,显示的过程中要求对背景进行模糊效果,且保持背景的动态性,即实现一个实时性的模糊效果(实时地对聊天或者弹幕内容进行模糊)。在思考在Android中如何实现这种效果之前,首先应该了解,一张图片从清晰到模糊这个过程是如何转变的。
通常我们所说的图像其实是由一个个像素方块组成,每个像素方块都有一个对应的彩色数值,所有的像素方块都表现出自己的对应的颜色时,具体的图像就展示出来了。
在计算机中,图像中每个像素方块的彩色数值的保存通常是以数组或者矩阵的形式保存的,矩阵中的每个元素就是对应的ARGB值,因此很多关于图像的处理实际上都是对图像矩阵的处理,通过改变图像矩阵中的值从而改变图像的显示。假设一张有9个像素点的红色图片, 如下:在Android中可以通过Bitmap获取到对应的像素点的像素,举个例子,有这么一个drawable:
通过Bitmap就可以获取图像矩阵每个元素的ARGB值,这里取(2,2)和中心点,预期应该一个是FF00AAF7(蓝色),一个应该是FFF74131(红色)。
public class MainActivity extends AppCompatActivity {
private TextView mRect;
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rect);
mRect = findViewById(R.id.rect_view);
Drawable drawable = mRect.getBackground();
Bitmap bitmap = drawableToBitmap(drawable);
int cX = bitmap.getWidth()/2;
int cY = bitmap.getHeight()/2;
int pixel = bitmap.getPixel(2, 2);// ARGB
int red = Color.red(pixel);
int green = Color.green(pixel);
int blue = Color.blue(pixel);
int alpha = Color.alpha(pixel);
Log.e(TAG, "\nA:"+Integer.toHexString(alpha) + "\nR:"+Integer.toHexString(red)
+"\nG:"+ Integer.toHexString(green) + "\nB:"+Integer.toHexString(blue));
pixel = bitmap.getPixel(cX, cY);// ARGB
red = Color.red(pixel);
green = Color.green(pixel);
blue = Color.blue(pixel);
alpha = Color.alpha(pixel);
Log.e(TAG, "\nA:"+Integer.toHexString(alpha) + "\nR:"+Integer.toHexString(red)
+"\nG:"+ Integer.toHexString(green) + "\nB:"+Integer.toHexString(blue));
}
public Bitmap drawableToBitmap(Drawable drawable) {
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
drawable.draw(canvas);
return bitmap;
}
结果为:
A:ff
R:0
G:aa
B:f7
A:ff
R:f7
G:41
B:31
结果和预期的一样。(如果有些许偏差,可能与具体的系统分辨率有关)
上面提到过,对图像的处理很多实际上是对图像矩阵的处理,而这其中就有一个基本的矩阵操作改变,那就是卷积。卷积对应在图像矩阵中的操作就是将图像矩阵A和 一个矩阵B通过一些运算操作后形成新的矩阵C,矩阵C的长宽和原来的图像矩阵A相同,从而形成新的图像矩阵,也即在原来的图像上形成了新的图像。而矩阵B,就叫做卷积核,也称为滤波器。通常卷积核要有两个特点:
卷积核矩阵中各个元素的值总和为1
矩阵的长宽为奇数,即每行每列的元素个数为奇数,奇数个的元素就使得卷积核有一个卷积中心,比如3X3的卷积核的卷积中心就是对应中间 (1,1)的位置
下面就对图像矩阵如何做卷积计算做个说明。首先假设我们有图像矩阵A和卷积和B,生成的结果是和矩阵A长宽相同的矩阵C(这里暂先忽略卷积核总和为1的特性)。
对于第一个位置(0,0)的数值的计算,首先将卷积中心(即(1,1)的位置)和图像矩阵A(0,0)的位置重合。
填充后对于第一个位置的的数值计算就是将矩阵A和卷积核矩阵B对应的位置乘积再求和就是矩阵C第一个位置的数值。
通过这种方式,算出了图像矩阵C的第一个数值,剩余的数值就是以同样的方式计算出来的。
上面的方式讲述了卷积的操作,对应到对图像模糊处理过程中,其实就是找到一个合适的卷积核,通过与原图像矩阵进行卷积操作后得出新的矩阵,就是我们想要图像矩阵。
均值卷积核是一种实现图像模糊最简单的方式,矩阵中每个元素的值都相同,且总和为1。下面就用一个80*80的均值卷积核对前面的红蓝方块做一个模糊处理。
滤波模糊处理的原理其实就是这样,但是由于均滤波的处理较粗糙,下面看看更为常见的模糊处理方式。
均值滤波是一种粗糙的模糊处理方式,实现的模糊效果一般较差,而高斯滤波就是相对处理效果更佳的卷积核,高斯滤波卷积核中的元素服从二维高斯分布。
从图中可见位于中心的元素值较高,四周偏低,这就使得在模糊效果看起来就是从中心向四周扩散的效果,因此高斯模糊是最常见的模糊处理效果。
高斯函数的sigma是称为函数的半径,在模糊处理中,也可以理解为模糊的半径,sigma越大,高斯函数最顶点越低,那么图像就越模糊,因此可通过sigma大小设置模糊的程度。
而对于卷积核的大小,因为高斯函数所有取值均大于0,因此对卷积核大小一般没有特定的要求,通常取值为6*sigma+1。
特别注意的是,因为图像矩阵的长宽值一般都很大,加上和卷积核的矩阵运算,所以,对图像处理的时候算法复杂度往往较高,处理时间较耗时,因此在做模糊处理的时候通常需要考虑性能问题。
对于高斯模糊这种常见的需求,Github上实际上已经有很多成熟的开源库。
Blurry(https://github.com/wasabeef/Blurry)
BlurView(https://github.com/Dimezis/BlurView)
500px-android-blur(https://github.com/500px/500px-android-blur)
其基本原理都是使用高斯滤波对图像矩阵做处理,不同的是在实现的算法或者使用的图像处理方式可能不同。
开源库 | 实现方式 |
---|---|
Blurry | RenderScript + java (使用 Java 算法做 API < 17 的兼容) |
BlurView | RenderScript (API < 17 需要导入 v8 兼容包 ) |
500px-android-blur | RenderScript (已停止维护) |
一般的高斯模糊效果实现的对象都是一张图片,或者一个静态的显示,因此实现的模糊效果的原理也相对简单,就是将图片的像素转换为矩阵的表示方式,再做一个矩阵的乘积,计算出模糊后的像素点值,从而达到实现对图片进行模糊处理的效果。
对于静态的图片或显示,实现模糊效果的方式相对简单,但是在项目中的要求是个实时的显示,动态的界面,比如一个聊天的界面,或者一个弹幕效果的界面,类似如下图,背景是一个ListView可以随时滑动的。
随着背景View的滑动,模糊的内容也随着变化,这种模糊的内容实时性的改变的界面,该如何实现高斯模糊呢?
原理其实也很简单,对于动态的界面显示,归结到底也就是一张张连续的图片,或者连续静态界面组成,因此只要对每一张图片或者,每一个静态界面进行模糊,然后再连续地显示就可以了。
上面的各种开源库有的也支持了实时模糊的要求,但是对于应用到实际项目中,就不可避免带来一个问题,apk体积增大,因为项目中实际上运用到这个功能的地方不多,所以通过引入开源的库的方式就不是最好的方式。
既然知道原理,并且原理也相对简单,因此完全可以自己实现。
实时模糊的原理:对于动态的界面显示,归结到底也就是一张张连续的图片,或者连续静态界面组成,因此只要对每一张图片或者,每一个静态界面进行模糊,然后再连续地显示就可以了。
因此实现的大致的步骤可以分为三步:
获取界面每一次绘制的图片或者说是显示,转换为矩阵形式表示,在Android中就是获取对应的Bitmap
进行矩阵计算,得出模糊后的图片或显示
重新绘制UI,显示模糊后的效果
下面就根据这三个步骤进行说明。
在Android中,熟悉View的绘制流程的都知道,View的绘制draw方法最后都是在Canvas(翻译为画布)上进行的,而这个类有这样一个构造器,可以将指定的Bitmap传进入,而Bitmap相信大家都懂,就是一张图片在Android中表示,准确来说就是Bitmap就是用像素矩阵来表示一张图片。因此我们可以通过自己制定的Bitmap创建一个Canvas,让背景界面在绘制的时候绘制到制定的Canvas上,就可以通过指定的Canvas拿到绘制后的图片像素矩阵,也即Bitmap,就可以进行模糊计算了。
在自定义View中设置。
ViewTreeObserver.OnPreDrawListener mOnPreDrawListener ...
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
mDecorView = getActivityDecorView();
if (mDecorView != null) {
mDecorView.getViewTreeObserver().addOnPreDrawListener(mOnPreDrawListener);
}
}
}
@Override
protected void onDetachedFromWindow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
if (mDecorView != null) {
mDecorView.getViewTreeObserver().removeOnPreDrawListener(mOnPreDrawListener);
}
release();
}
super.onDetachedFromWindow();
}
可以对View绘制前进行监听,在绘制前首先创建好指定的Canvas和Bitmap。
//对bitmap进行初始化,并创建画布,分配需要的内核区域
if (mBlurringCanvas == null || mBlurredBitmap == null
|| mBlurredBitmap.getWidth() != scaledWidth
|| mBlurredBitmap.getHeight() != scaledHeight) {
releaseBitmap();
//SourceBitmap就是需要需要处理的Bitmap
mSourceBitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
mBlurredBitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
if (mBlurredBitmap == null || mSourceBitmap == null) {
return false;
}
//BlurringCanvas就是我们指定的Canvas
mBlurringCanvas = new Canvas(mSourceBitmap);
mBlurInputAllocation = Allocation.createFromBitmap(mRenderScript, mSourceBitmap, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
mBlurOutputAllocation = Allocation.createTyped(mRenderScript, mBlurInputAllocation.getType());
}
然后在View绘制前设置指定的Canvas。
private ViewTreeObserver.OnPreDrawListener mOnPreDrawListener = new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
...
//传入画布,就可以获取到需要模糊的View的bitmap
if (decor.getBackground() != null) {
decor.getBackground().draw(mBlurringCanvas);
}
decor.draw(mBlurringCanvas);
mBlurringCanvas.restoreToCount(rc);//恢复状态
mIsRendering = false;
//对 Bitmap 进行模糊处理
blur(mSourceBitmap, mBlurredBitmap);
//重新绘制
if (redrawBitmap) {
invalidate();
}
}
return true;
}
};
因为View的绘制在Activity中是从DecorView开始往下的,因此将制定的Canvas传入并调用draw方法进行绘制,等一次绘制完成后,整个Activity中DecorView包裹的界面像素信息就会存储在SourceBitmap中,接下来就可以对SourceBitmap进行高斯计算。
高斯模糊计算是一种对像素矩阵进行矩阵运算的计算,对于矩阵的计算通常复杂度都很高,因此就对计算性能有一定的要求,这里就使用Android中提供的一个用于密集计算任务的框架RenderScript,这个类主要的功能就是为一些密集计算或者计算量大的任务提供CPU和GPU支持,比如一些加密计算或者图像处理计算等。其有一个特定的关联类ScriptIntrinsicBlur,是专门使用RenderScript进行高斯计算的封装类。
值得注意的是,因RenderScript只支持API>=17 若想支持API 16可添加 android.support.v8.renderscript兼容包。
下面就是使用RenderScript对之前获取到的SourceBitmap进行计算。首先指定对应的输入输出Bitmap即RenderScript使用到的输入输出内存。
private Bitmap mSourceBitmap; //源bitmap
private Bitmap mBlurredBitmap; //模糊后的bitmap
private RenderScript mRenderScript; //高性能计算脚本类
private ScriptIntrinsicBlur mScriptIntrinsicBlur;//高斯模糊计算的脚本类
//内核计算分配的内存
private Allocation mBlurInputAllocation; //输入分配
private Allocation mBlurOutputAllocation; //输出分配
初始化和分配输入输出内存空间。
if (mDirty || mRenderScript == null) {
if (mRenderScript == null) {
//初始化计算脚本和高斯计算脚本类
mRenderScript = RenderScript.create(getContext());
mScriptIntrinsicBlur = ScriptIntrinsicBlur.create(mRenderScript, Element.U8_4(mRenderScript));
}
mScriptIntrinsicBlur.setRadius(radius);//模糊半径,范围 (0.0f,25.0f]
mDirty = false;
}
int width = getWidth();
int height = getHeight();
int scaledWidth = Math.max(1, (int) (width / scaleFactor));
int scaledHeight = Math.max(1, (int) (height / scaleFactor));
//对bitmap进行初始化,并创建画布,分配需要的内核区域
if (mBlurringCanvas == null || mBlurredBitmap == null
|| mBlurredBitmap.getWidth() != scaledWidth
|| mBlurredBitmap.getHeight() != scaledHeight) {
releaseBitmap();
mSourceBitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
mBlurredBitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
if (mBlurredBitmap == null || mSourceBitmap == null) {
return false;
}
mBlurringCanvas = new Canvas(mSourceBitmap);
mBlurInputAllocation = Allocation.createFromBitmap(mRenderScript, mSourceBitmap, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);
mBlurOutputAllocation = Allocation.createTyped(mRenderScript, mBlurInputAllocation.getType());
最后用ScriptIntrinsicBlur进行计算就可以了。
private void blur(Bitmap sourceBitmap, Bitmap blurredBitmap) {
mBlurInputAllocation.copyFrom(sourceBitmap); //把源bitmap拷贝到内核内存中
mScriptIntrinsicBlur.setInput(mBlurInputAllocation); //设置高斯计算的输入内存
mScriptIntrinsicBlur.forEach(mBlurOutputAllocation); //进行计算,并将计算结果输出到输出内存中
mBlurOutputAllocation.copyTo(blurredBitmap); //将输出内存的的bitmap拷贝给blurredBitmap 即模糊后的bitMap
}
这样后输出的blurredBitmap就是用sourceBitmap模糊计算后的结果。
由于前面获取到的blurredBitmap是对整个屏幕的UI模糊处理后的Bitmap,因此如果是只要显示部分模糊效果,例如之前的只是对ListView上半部做模糊,那么就需要进行裁剪,裁剪原理就是使用canvas的和矩形Rect结合处理就可以了。如果需要设置覆盖颜色,比如前面的例子覆盖了一层黑色透明,也可以在这里使用画笔进行设置。
private void drawBlurredBitmap(Canvas canvas, Bitmap blurredBitmap, int overlayColor) {
//截取需要显示的模糊区域的大小
if (blurredBitmap != null) {
mRectSrc.right = blurredBitmap.getWidth();
mRectSrc.bottom = blurredBitmap.getHeight();
mRectDst.right = getWidth();
mRectDst.bottom = getHeight();
canvas.drawBitmap(blurredBitmap, mRectSrc, mRectDst, null);
}
//再对模糊后的画布做一个覆盖色处理,默认为黑色透明
mPaint.setColor(overlayColor);
canvas.drawRect(mRectDst, mPaint);
}
思路流程如下:
由于模糊效果的显示特点,对一个模糊图像进行缩小显示在效果上不会有很明显的影响,但是确可以提高计算的性能,减少计算量,因此对Bitmap的大小进行适当的缩放可以提高一定的性能。
在实现中设置了一个缩放因子。
private static final float DEFAULT_SCALE_FACTOR = 4;
private float mScaleFactor;// 缩放因子(缩小后对模糊效果影响小但能提高性能)
以下是当缩放因子为1和4的效率对比。
缩放因子 | CPU 占用率 | 方法耗时 |
---|---|---|
1 | 51.6 % | 43.69ms |
4 | 9% | 2.55ms |
红米 3S机型,MIUI 10.2版本
可见对Bitmap进行缩小对性能可以有较高的提高。
在缩放因子为4的时候,使用RenderScript实现模糊处理后,滑动ListView时CPU和内存占用百分比。
这是相同的界面,不过使用一个半透明TextView替换模糊背景的效果。
可以看到对CPU的占用率和内存的占用率的增长都不是很大,因此使用RenderScript带来的性能开销是可以接受的。
最后附上Github地址:
RealTimeBlurryView(https://github.com/yishengma/RealTimeBlurryView)
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。