@[TOC]
最近在项目开发中遇到一个需求,需要满足两种情况:
1.在应用的某个界面会弹出透明对话框,对话框的背景高斯模糊,界面其他部分正常;
2.在界面中有一个透明侧拉菜单,在侧拉菜单滑动的过程总,菜单背景会随之高斯模糊;
当时碰到这个需求的时候也是感觉挺懵逼的,因为以前也没有做过高斯模糊这种效果,后来看了很多博客,慢慢了解了一些,在这里总结一下, 希望以后有童鞋需要这些效果的时候,也能提供些帮助。
效果也看得差不多了,说说实现的过程吧,不过关于高斯模糊的原理这里就不详细介绍了,有兴趣的同学可以去看看:
阮一峰的网络日志——高斯模糊的算法。
下面我们来看看用代码是怎么实现的吧。网上有很多高斯模糊的算法, 需要的同学可以去搜一搜,这里我先贴出来一种:
public static Bitmap doBlur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap) { Bitmap bitmap; if (canReuseInBitmap) { bitmap = sentBitmap; } else { bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); } if (radius < 1) { return (null); } int w = bitmap.getWidth(); int h = bitmap.getHeight(); int[] pix = new int[w * h]; bitmap.getPixels(pix, 0, w, 0, 0, w, h); int wm = w - 1; int hm = h - 1; int wh = w * h; int div = radius + radius + 1; int r[] = new int[wh]; int g[] = new int[wh]; int b[] = new int[wh]; int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; int vmin[] = new int[Math.max(w, h)]; int divsum = (div + 1) >> 1; divsum *= divsum; int dv[] = new int[256 * divsum]; for (i = 0; i < 256 * divsum; i++) { dv[i] = (i / divsum); } yw = yi = 0; int[][] stack = new int[div][3]; int stackpointer; int stackstart; int[] sir; int rbs; int r1 = radius + 1; int routsum, goutsum, boutsum; int rinsum, ginsum, binsum; for (y = 0; y < h; y++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; for (i = -radius; i <= radius; i++) { p = pix[yi + Math.min(wm, Math.max(i, 0))]; sir = stack[i + radius]; sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff); rbs = r1 - Math.abs(i); rsum += sir[0] * rbs; gsum += sir[1] * rbs; bsum += sir[2] * rbs; if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; } } stackpointer = radius; for (x = 0; x < w; x++) { r[yi] = dv[rsum]; g[yi] = dv[gsum]; b[yi] = dv[bsum]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; sir = stack[stackstart % div]; routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]; if (y == 0) { vmin[x] = Math.min(x + radius + 1, wm); } p = pix[yw + vmin[x]]; sir[0] = (p & 0xff0000) >> 16; sir[1] = (p & 0x00ff00) >> 8; sir[2] = (p & 0x0000ff); rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; sir = stack[(stackpointer) % div]; routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]; yi++; } yw += w; } for (x = 0; x < w; x++) { rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; yp = -radius * w; for (i = -radius; i <= radius; i++) { yi = Math.max(0, yp) + x; sir = stack[i + radius]; sir[0] = r[yi]; sir[1] = g[yi]; sir[2] = b[yi]; rbs = r1 - Math.abs(i); rsum += r[yi] * rbs; gsum += g[yi] * rbs; bsum += b[yi] * rbs; if (i > 0) { rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; } else { routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; } if (i < hm) { yp += w; } } yi = x; stackpointer = radius; for (y = 0; y < h; y++) { pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; rsum -= routsum; gsum -= goutsum; bsum -= boutsum; stackstart = stackpointer - radius + div; sir = stack[stackstart % div]; routsum -= sir[0]; goutsum -= sir[1]; boutsum -= sir[2]; if (x == 0) { vmin[y] = Math.min(y + r1, hm) * w; } p = x + vmin[y]; sir[0] = r[p]; sir[1] = g[p]; sir[2] = b[p]; rinsum += sir[0]; ginsum += sir[1]; binsum += sir[2]; rsum += rinsum; gsum += ginsum; bsum += binsum; stackpointer = (stackpointer + 1) % div; sir = stack[stackpointer]; routsum += sir[0]; goutsum += sir[1]; boutsum += sir[2]; rinsum -= sir[0]; ginsum -= sir[1]; binsum -= sir[2]; yi += w; } } bitmap.setPixels(pix, 0, w, 0, 0, w, h); return bitmap; }好了。算法也了解了一点了,我们来看看具体效果是怎么实现的吧。
效果图
activity_normal_blur.xml布局很简单,就只是一个Button加上ImageView,代码如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv_normal_blur" android:layout_width="match_parent" android:layout_height="match_parent" android:contentDescription="@string/normal_blur_iv_description" android:scaleType="fitXY" android:src="@drawable/pic"/> <Button android:id="@+id/btn_normal_blurred" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="高斯模糊"/> </RelativeLayout>然后在Activity点击Button来控制图片是否进行高斯模糊,图片在进行高斯模糊的时候调用了BlurBitmapUtil中的blurBitmap()方法:
mIv = (ImageView) findViewById(R.id.iv_normal_blur); mChangeBtn = (Button) findViewById(R.id.btn_normal_blurred); mChangeBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (!mShow) { mIv.buildDrawingCache(); Bitmap sentBitmap = mIv.getDrawingCache(); mIv.setImageBitmap(BlurBitmapUtil.blurBitmap(NormalBlurredActivity.this, sentBitmap, 20.0f)); mChangeBtn.setText("取消高斯模糊"); mShow = true; }else { Log.e("tag","1"); mIv.setImageResource(R.drawable.pic); mChangeBtn.setText("高斯模糊"); Log.e("tag","2"); mShow = false; } } });其中调用的BlurBitmapUtil中的blurBitmap方法也是计算高斯模糊的一种算法,不过需要SDK的版本不能低于17,代码如下:
@SuppressLint("NewApi") public static Bitmap blurBitmap(Context context, Bitmap image, float blurRadius) { // 计算图片缩小后的长宽 int width = Math.round(image.getWidth() * BITMAP_SCALE); int height = Math.round(image.getHeight() * BITMAP_SCALE); // 将缩小后的图片做为预渲染的图片。 Bitmap inputBitmap = Bitmap.createScaledBitmap(image, width, height, false); // 创建一张渲染后的输出图片。 Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap); // 创建RenderScript内核对象 RenderScript rs = RenderScript.create(context); // 创建一个模糊效果的RenderScript的工具对象 ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); // 由于RenderScript并没有使用VM来分配内存,所以需要使用Allocation类来创建和分配内存空间。 // 创建Allocation对象的时候其实内存是空的,需要使用copyTo()将数据填充进去。 Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap); Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap); // 设置渲染的模糊程度, 25f是最大模糊度 blurScript.setRadius(blurRadius); // 设置blurScript对象的输入内存 blurScript.setInput(tmpIn); // 将输出数据保存到输出内存中 blurScript.forEach(tmpOut); // 将数据填充到Allocation中 tmpOut.copyTo(outputBitmap); return outputBitmap; }在这里使用了一个RenderScript的类,这个类是Android平台上进行高性能计算的框架,使用方法就在上面了,至于详细的介绍有兴趣的同学可以去查阅一下官方文档,这里就不多说了。
高斯模糊的效果算是实现了,但是·在很多时候,这种一般的高斯模糊效果很难满足我们的需求。比如说我们只需要界面中的某一部分进行高斯模糊怎么办呢?,就比如说下面这种效果:在弹出对话框的时候只有对话框那一部分是高斯模糊,其他部分正常显示。
这种效果有该怎么来操作呢,这个也没有想象中那么复杂,只不过是需要利用截图而已,来,我们先看一下activity_local_blur.xml布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/local_blur_root" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/pic" tools:context="com.zgd.gaussblur.MainActivity"> <Button android:id="@+id/btn_local_blur" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/local_blur_show_dialog"/> <FrameLayout android:id="@+id/fragment_mydialog_root" android:layout_width="200dp" android:layout_height="wrap_content" android:layout_centerInParent="true" android:visibility="invisible"></FrameLayout> </RelativeLayout>
在这里,我使用的了一个Fragment来进行替换不居中的FrameLayout,用来模拟弹出对话框。这个Fragment的代码逻辑和布局特别简单,这里就不贴出来了,有兴趣的同学可以看看源码。我们看看在这个Activity中是怎么操作的吧,将Fragment显示出来之后,这时候调用View.getDrawingCache()来获取屏幕截图,不过在这之前还需要调用一下View的buildDrawingCache()方法。具体逻辑写在了BlurBitmapUtil类里面了,代码上面已经贴出来了,这里就不多说了,看看Activity中是怎么操作的:
mLocalBlurRootView = findViewById(R.id.local_blur_root); mBtn = (Button) findViewById(R.id.btn_local_blur); mFragmentRootView = findViewById(R.id.fragment_mydialog_root); mBtn.setOnClickListener(new OnClickListener() { @SuppressLint("NewApi") @Override public void onClick(View v) { FragmentTransaction ft = getFragmentManager().beginTransaction(); if (show) { if (mFragment != null) { ft.remove(mFragment).commit(); } mFragmentRootView.setVisibility(View.INVISIBLE); mBtn.setText("弹出对话框"); }else{ if (mFragment == null) { mFragment = new MyDialogFragment(); } ft.add(R.id.fragment_mydialog_root, mFragment).commit(); ViewTreeObserver vto = mLocalBlurRootView.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { mLocalBlurRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this); BlurBitmapUtil.blur(mLocalBlurRootView, mFragmentRootView, 5, 8); } }); mBtn.setText("取消对话框"); mFragmentRootView.setVisibility(View.VISIBLE); } show = !show; } }); mBtn.setOnClickListener(new OnClickListener() { @SuppressLint("NewApi") @Override public void onClick(View v) { FragmentTransaction ft = getFragmentManager().beginTransaction(); if (show) { if (mFragment != null) { ft.remove(mFragment).commit(); } mFragmentRootView.setVisibility(View.INVISIBLE); mBtn.setText("弹出对话框"); }else{ if (mFragment == null) { mFragment = new MyDialogFragment(); } ft.add(R.id.fragment_mydialog_root, mFragment).commit(); ViewTreeObserver vto = mLocalBlurRootView.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { mLocalBlurRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this); BlurBitmapUtil.blur(mLocalBlurRootView, mFragmentRootView, 5, 8); } }); mBtn.setText("取消对话框"); mFragmentRootView.setVisibility(View.VISIBLE); } show = !show; } });
@SuppressWarnings("deprecation") @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public static void blur(View fromView, View toView, float radius, float scaleFactor) { // 获取View的截图 fromView.buildDrawingCache(); Bitmap bkg = fromView.getDrawingCache(); if (radius < 1 || radius > 26) { scaleFactor = 8; radius = 2; } Bitmap overlay = Bitmap.createBitmap( (int) (toView.getWidth() / scaleFactor), (int) (toView.getHeight() / scaleFactor), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(overlay); canvas.translate(-toView.getLeft() / scaleFactor, -toView.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 = doBlur(overlay, (int) radius, true); // 为对应View设置背景 toView.setBackground(new BitmapDrawable(overlay)); }
局部的高斯模糊效果实现了,但是我们想,可不可以将图片或者背景进行动态的高斯模糊呢,后来看到了
iamxiarui的博客——Android:简单靠谱的动态高斯模糊效果
找到了一个自定义的高斯模糊View,可以极其方便的对图片进行动态的高斯模糊。
这个自定义的View不居中有两个ImageView,后面的ImageView将彻底高斯模糊的图片作为Image,而前面的ImageView将正常的图片作为Image,其中也只有四个比较核心的方法,我贴出来看一下:
private void initView(Context context) { mContext = context; LayoutInflater.from(context).inflate(R.layout.blurredview, this); mOriginImg = (ImageView) findViewById(R.id.blurredview_origin_img); mBlurredImg = (ImageView) findViewById(R.id.blurredview_blurred_img); } /** * 初始化属性 * * @param context * 上下文对象 * @param attrs * 相关属性 */ private void initAttr(Context context, AttributeSet attrs) { // 查找属性值 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BlurredView); Drawable drawable = typedArray.getDrawable(R.styleable.BlurredView_src); // 默认为false isDisableBlurred = typedArray.getBoolean( R.styleable.BlurredView_disableBlurred, false); // 必须回收 方便重用 typedArray.recycle(); // 模糊图片 if (null != drawable) { mOriginBitmap = BitmapUtil.drawableToBitmap(drawable); mBlurredBitmap = BlurBitmapUtil.blurBitmap(context, mOriginBitmap, BLUR_RADIUS); } // 设置是否可见 if (!isDisableBlurred) { mBlurredImg.setVisibility(VISIBLE); } } private void setImageView() { mBlurredImg.setImageBitmap(mBlurredBitmap); mOriginImg.setImageBitmap(mOriginBitmap); } @SuppressWarnings("deprecation") public void setBlurredLevel(int level) { // 超过模糊级别范围 直接抛异常 if (level < 0 || level > 100) { throw new IllegalStateException( "No validate level, the value must be 0~100"); } // 禁用模糊直接返回 if (isDisableBlurred) { return; } // 设置透明度 mOriginImg.setAlpha((int) (ALPHA_MAX_VALUE - level * 2.55)); }这样只需要操作上层ImageView的透明度就可以很巧妙的对图片进行动态的高斯模糊处理,好了,自定义View写好了,接下来开始处理动态高斯模糊的Activity。activity_dynamic_blur.xml布局就是使用了一个SeekBar加上自定义的BlurView,代码如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" > <SeekBar android:id="@+id/seekbar_dynamic_blur" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:layout_marginBottom="16dp"/> <com.zgd.gaussblur.view.BlurredView android:id="@+id/blurredview_dynamic_blur" android:layout_below="@id/seekbar_dynamic_blur" android:layout_width="match_parent" android:layout_height="match_parent" app:src="@drawable/pic" android:scaleType="fitXY"/> </RelativeLayout>由于使用了自定义的BlurView,所以这个Activity中的逻辑就比较简单了,在SeekBar滑动的过程中,根据进度条的变化,直接调用BlurView的setBlurredLevel(progress)方法就可以动态的设置图片的高斯模糊程度了:
blurredView = (BlurredView) findViewById(R.id.blurredview_dynamic_blur); mSeekBar = (SeekBar) findViewById(R.id.seekbar_dynamic_blur); mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onStopTrackingTouch(SeekBar seekBar) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @SuppressLint("NewApi") @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { blurredView.setBlurredLevel(progress); } });blurredView = (BlurredView) findViewById(R.id.blurredview_dynamic_blur); mSeekBar = (SeekBar) findViewById(R.id.seekbar_dynamic_blur); mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onStopTrackingTouch(SeekBar seekBar) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @SuppressLint("NewApi") @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { blurredView.setBlurredLevel(progress); } });效果图如下:
前面几种效果也都实现了,但是还是不能够满足项目的需求,怎么才能让侧拉菜单滑动的过程中,背景也能随之动态的高斯模糊呢,别急,下面我们就来看看这种效果该如何实现。
首先,我们先在Activity中构建好一个侧拉菜单,这个我就不说了,直接生成一个带有侧拉菜单的Activity就直接可以用了。
剩下的工作其实也不难了,原理跟上面的都是一样的,使用的工具类也相同。只需要在Activity中添加如下几行代码就可以搞定:
final View view = findViewById(R.id.drawer_bg); navigationView.getViewTreeObserver() .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { BlurBitmapUtil.blur(view, navigationView, 4, 8); return true; } });这里我们只需要在侧拉菜单滑动的过程中动态的调用BlurBitmap中的blur方法即可。
然后。。。然后。。。就没有然后了,效果已经实现了。
对,没错,就这样就实现了。看起来很复杂的东西,其实在我们前面的学习过程中已经把这个需要的工具类和方法都已经写好了。我们实现这最后一个效果也只需要进行调用就可以。
原理都是一样的,只要明白了其中的操作方法和实现原理,不管是需要什么样的高斯模糊效果,相信都可以实现了。
好了,今天就写到这里,希望能够对大家有所帮助。有些不完善的地方,也欢迎大家批评指正。
代码已经上传到Github上了,有需要的同学可以进行下载。欢迎Star,欢迎Fork。