1. 程式人生 > >Android區域性動態高斯模糊以及側滑選單配合高斯模糊

Android區域性動態高斯模糊以及側滑選單配合高斯模糊

最近在專案開發中遇到一個需求,需要滿足兩種情況:

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;
			}
		});

    @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);
		}
	});

效果圖如下:



實現側拉選單配合高斯模糊

前面幾種效果也都實現了,但是還是不能夠滿足專案的需求,怎麼才能讓側拉選單滑動的過程中,背景也能隨之動態的高斯模糊呢,別急,下面我們就來看看這種效果該如何實現。 首先,我們先在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。

點選下載原始碼