package com.marshalchen.common.uimodule.blurdialogfragment;
import android.app.Activity;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.os.AsyncTask;
import android.os.Build;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
/**
* Encapsulate the whole behaviour to provide a blur effect on a DialogFragment.
* <p/>
* All the screen behind the dialog will be blurred except the action bar.
* <p/>
* Simply linked all methods to the matching lifecycle ones.
*/
public class BlurDialogEngine {
/**
* Log cat
*/
private static final String TAG = BlurDialogEngine.class.getSimpleName();
/**
* Since image is going to be blurred, we don't care about resolution.
* Down scale factor to reduce blurring time and memory allocation.
*/
private static final float BLUR_DOWN_SCALE_FACTOR = 4.0f;
/**
* Radius used to blur the background
*/
private static final int BLUR_RADIUS = 8;
/**
* Image view used to display blurred background.
*/
private ImageView mBlurredBackgroundView;
/**
* Layout params used to add blurred background.
*/
private FrameLayout.LayoutParams mBlurredBackgroundLayoutParams;
/**
* Task used to capture screen and blur it.
*/
private BlurAsyncTask mBluringTask;
/**
* Used to enable or disable debug mod.
*/
private boolean mDebudEnable = false;
/**
* Factor used to down scale background. High quality isn't necessary
* since the background will be blurred.
*/
private float mDownScaleFactor = BLUR_DOWN_SCALE_FACTOR;
/**
* Radius used for fast blur algorithm.
*/
private int mBlurRadius = BLUR_RADIUS;
/**
* Holding activity.
*/
private Activity mHoldingActivity;
/**
* Constructor.
*
* @param holdingActivity activity which holds the DialogFragment.
*/
public BlurDialogEngine(Activity holdingActivity) {
mHoldingActivity = holdingActivity;
}
/**
* Resume the engine.
*
* @param retainedInstance use getRetainInstance.
*/
public void onResume(boolean retainedInstance) {
if (mBlurredBackgroundView == null || retainedInstance) {
mBluringTask = new BlurAsyncTask();
mBluringTask.execute();
}
}
/**
* Must be linked to the original lifecycle.
*/
public void onDismiss() {
//remove blurred background and clear memory, could be null if dismissed before blur effect
//processing ends
if (mBlurredBackgroundView != null) {
mBlurredBackgroundView.setVisibility(View.GONE);
mBlurredBackgroundView = null;
}
//cancel async task
mBluringTask.cancel(true);
mBluringTask = null;
}
/**
* Must be linked to the original lifecycle.
*/
public void onDestroy() {
mHoldingActivity = null;
}
/**
* Enable / disable debug mode.
* <p/>
* LogCat and graphical information directly on blurred screen.
*
* @param enable true to display log in LogCat.
*/
public void debug(boolean enable) {
mDebudEnable = enable;
}
/**
* Apply custom down scale factor.
* <p/>
* By default down scale factor is set to
* {@link com.marshalchen.common.uimodule.blurdialogfragment.BlurDialogEngine#BLUR_DOWN_SCALE_FACTOR}
* <p/>
* Higher down scale factor will increase blurring speed but reduce final rendering quality.
*
* @param factor customized down scale factor, must be at least 1.0 ( no down scale applied )
*/
public void setDownScaleFactor(float factor) {
if (factor >= 1.0f) {
mDownScaleFactor = factor;
} else {
mDownScaleFactor = 1.0f;
}
}
/**
* Apply custom blur radius.
* <p/>
* By default blur radius is set to
* {@link com.marshalchen.common.uimodule.blurdialogfragment.BlurDialogEngine#BLUR_RADIUS}
*
* @param radius custom radius used to blur.
*/
public void setBlurRadius(int radius) {
if (radius >= 0) {
mBlurRadius = radius;
} else {
mBlurRadius = 0;
}
}
/**
* Blur the given bitmap and add it to the activity.
*
* @param bkg should be a bitmap of the background.
* @param view background view.
*/
private void blur(Bitmap bkg, View view) {
long startMs = System.currentTimeMillis();
//define layout params to the previous imageView in order to match its parent
mBlurredBackgroundLayoutParams = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
);
//overlay used to build scaled preview and blur background
Bitmap overlay = null;
//evaluate top offset due to action bar
int actionBarHeight = 0;
try {
if (mHoldingActivity instanceof ActionBarActivity) {
ActionBar supportActionBar
= ((ActionBarActivity) mHoldingActivity).getSupportActionBar();
if (supportActionBar != null) {
actionBarHeight = supportActionBar.getHeight();
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
android.app.ActionBar actionBar = mHoldingActivity.getActionBar();
if (actionBar != null) {
actionBarHeight = actionBar.getHeight();
}
}
} catch (NoClassDefFoundError e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
android.app.ActionBar actionBar = mHoldingActivity.getActionBar();
if (actionBar != null) {
actionBarHeight = actionBar.getHeight();
}
}
}
//evaluate top offset due to status bar
int statusBarHeight = 0;
if ((mHoldingActivity.getWindow().getAttributes().flags
& WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0) {
//not in fullscreen mode
statusBarHeight = getStatusBarHeight();
}
final int topOffset = actionBarHeight + statusBarHeight;
final int bottomOffset = getNavigationBarOffset();
//add offset to the source boundaries since we don't want to blur actionBar pixels
Rect srcRect = new Rect(
0,
actionBarHeight + statusBarHeight,
bkg.getWidth(),
bkg.getHeight() - bottomOffset
);
//in order to keep the same ratio as the one which will be used for rendering, also
//add the offset to the overlay.
overlay = Bitmap.createBitmap((int) ((view.getWidth()) / mDownScaleFactor),
(int) ((view.getMeasuredHeight() - topOffset - bottomOffset) / mDownScaleFactor),
Bitmap.Config.RGB_565);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
|| mHoldingActivity instanceof ActionBarActivity) {
//add offset as top margin since actionBar height must also considered when we display
// the blurred background. Don't want to draw on the actionBar.
mBlurredBackgroundLayoutParams.setMargins(
0,
actionBarHeight,
0,
0
);
mBlurredBackgroundLayoutParams.gravity = Gravity.TOP;
}
//scale and draw background view on the canvas overlay
Canvas canvas = new Canvas(overlay);
Paint paint = new Paint();
paint.setFlags(Paint.FILTER_BITMAP_FLAG);
//build drawing destination boundaries
final RectF destRect = new RectF(0, 0, overlay.getWidth(), overlay.getHeight());
//draw background from source area in source background to the destination area on the overlay
canvas.drawBitmap(bkg, srcRect, destRect, paint);
//apply fast blur on overlay
overlay = FastBlurHelper.doBlur(overlay, mBlurRadius, false);
if (mDebudEnable) {
String blurTime = (System.currentTimeMillis() - startMs) + " ms";
//display information in LogCat
Log.d(TAG, "Radius : " + mBlurRadius);
Log.d(TAG, "Down Scale Factor : " + mDownScaleFactor);
Log.d(TAG, "Blurred achieved in : " + blurTime);
Log.d(TAG, "Allocation : " + bkg.getRowBytes() + "ko (screen capture) + "
+ overlay.getRowBytes() + "ko (FastBlur)");
//display blurring time directly on screen
Rect bounds = new Rect();
Canvas canvas1 = new Canvas(overlay);
paint.setColor(Color.BLACK);
paint.setAntiAlias(true);
paint.setTextSize(20.0f);
paint.getTextBounds(blurTime, 0, blurTime.length(), bounds);
canvas1.drawText(blurTime, 2, bounds.height(), paint);
}
//set bitmap in an image view for final rendering
mBlurredBackgroundView = new ImageView(mHoldingActivity);
mBlurredBackgroundView.setImageDrawable(new BitmapDrawable(mHoldingActivity.getResources(), overlay));
}
/**
* retrieve status bar height in px
*
* @return status bar height in px
*/
private int getStatusBarHeight() {
int result = 0;
int resourceId = mHoldingActivity.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = mHoldingActivity.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
/**
* Retrieve offset introduce by the navigation bar.
*
* @return bottom offset due to navigation bar.
*/
private int getNavigationBarOffset() {
int result = 0;
Resources resources = mHoldingActivity.getResources();
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP
&& resources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0) {
result = resources.getDimensionPixelSize(resourceId);
}
}
return result;
}
/**
* Async task used to process blur out of ui thread
*/
public class BlurAsyncTask extends AsyncTask<Void, Void, Void> {
private Bitmap mBackground;
private View mBackgroundView;
@Override
protected void onPreExecute() {
super.onPreExecute();
mBackgroundView = mHoldingActivity.getWindow().getDecorView();
//retrieve background view, must be achieved on ui thread since
//only the original thread that created a view hierarchy can touch its views.
Rect rect = new Rect();
mBackgroundView.getWindowVisibleDisplayFrame(rect);
mBackgroundView.destroyDrawingCache();
mBackgroundView.setDrawingCacheEnabled(true);
mBackgroundView.buildDrawingCache(true);
mBackground = mBackgroundView.getDrawingCache(true);
/**
* After rotation, the DecorView has no height and no width. Therefore
* .getDrawingCache() return null. That's why we have to force measure and layout.
*/
if (mBackground == null) {
mBackgroundView.measure(
View.MeasureSpec.makeMeasureSpec(rect.width(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(rect.height(), View.MeasureSpec.EXACTLY)
);
mBackgroundView.layout(0, 0, mBackgroundView.getMeasuredWidth(),
mBackgroundView.getMeasuredHeight());
mBackgroundView.destroyDrawingCache();
mBackgroundView.setDrawingCacheEnabled(true);
mBackgroundView.buildDrawingCache(true);
mBackground = mBackgroundView.getDrawingCache(true);
}
}
@Override
protected Void doInBackground(Void... params) {
//process to the blue
blur(mBackground, mBackgroundView);
//clear memory
mBackground.recycle();
mBackgroundView.destroyDrawingCache();
mBackgroundView.setDrawingCacheEnabled(false);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
mHoldingActivity.getWindow().addContentView(
mBlurredBackgroundView,
mBlurredBackgroundLayoutParams
);
mBackgroundView = null;
mBackground = null;
}
}
}