package com.flurgle.blurkit; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.util.AttributeSet; import android.view.Choreographer; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import java.lang.ref.WeakReference; /** * A {@link ViewGroup} that blurs all content behind it. Automatically creates bitmap of parent content * and finds its relative position to the top parent to draw properly regardless of where the layout is * placed. */ public class BlurLayout extends FrameLayout { public static final float DEFAULT_DOWNSCALE_FACTOR = 0.12f; public static final int DEFAULT_BLUR_RADIUS = 12; public static final int DEFAULT_FPS = 60; public static final float DEFAULT_CORNER_RADIUS = 0.f; // Customizable attributes /** Factor to scale the view bitmap with before blurring. */ private float mDownscaleFactor; /** Blur radius passed directly to stackblur library. */ private int mBlurRadius; /** Number of blur invalidations to do per second. */ private int mFPS; /** Corner radius for the layouts blur. To make rounded rects and circles. */ private float mCornerRadius; /** Is blur running? */ private boolean mRunning; /** Is window attached? */ private boolean mAttachedToWindow; /** Do we need to recalculate the position each invalidation? */ private boolean mPositionLocked; /** Do we need to regenerate the view bitmap each invalidation? */ private boolean mViewLocked; // Calculated class dependencies /** ImageView to show the blurred content. */ private RoundedImageView mImageView; /** Reference to View for top-parent. For retrieval see {@link #getActivityView() getActivityView}. */ private WeakReference<View> mActivityView; /** A saved point to re-use when {@link #lockPosition()} called. */ private Point mLockedPoint; /** A saved bitmap for the view to re-use when {@link #lockView()} called. */ private Bitmap mLockedBitmap; public BlurLayout(Context context) { super(context, null); } public BlurLayout(Context context, AttributeSet attrs) { super(context, attrs); if (!isInEditMode()) { com.flurgle.blurkit.BlurKit.init(context); } TypedArray a = context.getTheme().obtainStyledAttributes( attrs, com.flurgle.blurkit.R.styleable.BlurLayout, 0, 0); try { mDownscaleFactor = a.getFloat(R.styleable.BlurLayout_blk_downscaleFactor, DEFAULT_DOWNSCALE_FACTOR); mBlurRadius = a.getInteger(R.styleable.BlurLayout_blk_blurRadius, DEFAULT_BLUR_RADIUS); mFPS = a.getInteger(R.styleable.BlurLayout_blk_fps, DEFAULT_FPS); mCornerRadius = a.getDimension(R.styleable.BlurLayout_blk_cornerRadius, DEFAULT_CORNER_RADIUS); } finally { a.recycle(); } mImageView = new RoundedImageView(getContext()); mImageView.setScaleType(ImageView.ScaleType.FIT_XY); addView(mImageView); setCornerRadius(mCornerRadius); } /** Choreographer callback that re-draws the blur and schedules another callback. */ private Choreographer.FrameCallback invalidationLoop = new Choreographer.FrameCallback() { @Override public void doFrame(long frameTimeNanos) { invalidate(); Choreographer.getInstance().postFrameCallbackDelayed(this, 1000 / mFPS); } }; /** Start BlurLayout continuous invalidation. **/ public void startBlur() { if (mRunning) { return; } if (mFPS > 0) { mRunning = true; Choreographer.getInstance().postFrameCallback(invalidationLoop); } } /** Pause BlurLayout continuous invalidation. **/ public void pauseBlur() { if (!mRunning) { return; } mRunning = false; Choreographer.getInstance().removeFrameCallback(invalidationLoop); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mAttachedToWindow = true; startBlur(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mAttachedToWindow = false; pauseBlur(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); invalidate(); } @Override public void invalidate() { super.invalidate(); Bitmap bitmap = blur(); if (bitmap != null) { mImageView.setImageBitmap(bitmap); } } /** * Recreates blur for content and sets it as the background. */ private Bitmap blur() { if (getContext() == null || isInEditMode()) { return null; } // Check the reference to the parent view. // If not available, attempt to make it. if (mActivityView == null || mActivityView.get() == null) { mActivityView = new WeakReference<>(getActivityView()); if (mActivityView.get() == null) { return null; } } Point pointRelativeToActivityView; if (mPositionLocked) { // Generate a locked point if null. if (mLockedPoint == null) { mLockedPoint = getPositionInScreen(); } // Use locked point. pointRelativeToActivityView = mLockedPoint; } else { // Calculate the relative point to the parent view. pointRelativeToActivityView = getPositionInScreen(); } // Set alpha to 0 before creating the parent view bitmap. // The blur view shouldn't be visible in the created bitmap. setAlpha(0); // Screen sizes for bound checks int screenWidth = mActivityView.get().getWidth(); int screenHeight = mActivityView.get().getHeight(); // The final dimensions of the blurred bitmap. int width = (int) (getWidth() * mDownscaleFactor); int height = (int) (getHeight() * mDownscaleFactor); // The X/Y position of where to crop the bitmap. int x = (int) (pointRelativeToActivityView.x * mDownscaleFactor); int y = (int) (pointRelativeToActivityView.y * mDownscaleFactor); // Padding to add to crop pre-blur. // Blurring straight to edges has side-effects so padding is added. int xPadding = getWidth() / 8; int yPadding = getHeight() / 8; // Calculate padding independently for each side, checking edges. int leftOffset = -xPadding; leftOffset = x + leftOffset >= 0 ? leftOffset : 0; int rightOffset = xPadding; rightOffset = x + getWidth() + rightOffset <= screenWidth ? rightOffset : screenWidth - getWidth() - x; int topOffset = -yPadding; topOffset = y + topOffset >= 0 ? topOffset : 0; int bottomOffset = yPadding; bottomOffset = y + height + bottomOffset <= screenHeight ? bottomOffset : 0; // Parent view bitmap, downscaled with mDownscaleFactor Bitmap bitmap; if (mViewLocked) { // It's possible for mLockedBitmap to be null here even with view locked. // lockView() should always properly set mLockedBitmap if this code is reached // (it passed previous checks), so recall lockView and assume it's good. if (mLockedBitmap == null) { lockView(); } if (width == 0 || height == 0) { return null; } bitmap = Bitmap.createBitmap(mLockedBitmap, x, y, width, height); } else { try { // Create parent view bitmap, cropped to the BlurLayout area with above padding. bitmap = getDownscaledBitmapForView( mActivityView.get(), new Rect( pointRelativeToActivityView.x + leftOffset, pointRelativeToActivityView.y + topOffset, pointRelativeToActivityView.x + getWidth() + Math.abs(leftOffset) + rightOffset, pointRelativeToActivityView.y + getHeight() + Math.abs(topOffset) + bottomOffset ), mDownscaleFactor ); } catch (com.flurgle.blurkit.BlurKitException e) { return null; } catch (NullPointerException e) { return null; } } if (!mViewLocked) { // Blur the bitmap. bitmap = com.flurgle.blurkit.BlurKit.getInstance().blur(bitmap, mBlurRadius); //Crop the bitmap again to remove the padding. bitmap = Bitmap.createBitmap( bitmap, (int) (Math.abs(leftOffset) * mDownscaleFactor), (int) (Math.abs(topOffset) * mDownscaleFactor), width, height ); } // Make self visible again. setAlpha(1); // Set background as blurred bitmap. return bitmap; } /** * Casts context to Activity and attempts to create a view reference using the window decor view. * @return View reference for whole activity. */ private View getActivityView() { Activity activity; try { activity = (Activity) getContext(); } catch (ClassCastException e) { return null; } return activity.getWindow().getDecorView().findViewById(android.R.id.content); } /** * Returns the position in screen. Left abstract to allow for specific implementations such as * caching behavior. */ private Point getPositionInScreen() { PointF pointF = getPositionInScreen(this); return new Point((int) pointF.x, (int) pointF.y); } /** * Finds the Point of the parent view, and offsets result by self getX() and getY(). * @return Point determining position of the passed in view inside all of its ViewParents. */ private PointF getPositionInScreen(View view) { if (getParent() == null) { return new PointF(); } ViewGroup parent; try { parent = (ViewGroup) view.getParent(); } catch (Exception e) { return new PointF(); } if (parent == null) { return new PointF(); } PointF point = getPositionInScreen(parent); point.offset(view.getX(), view.getY()); return point; } /** * Users a View reference to create a bitmap, and downscales it using the passed in factor. * Uses a Rect to crop the view into the bitmap. * @return Bitmap made from view, downscaled by downscaleFactor. * @throws NullPointerException */ private Bitmap getDownscaledBitmapForView(View view, Rect crop, float downscaleFactor) throws com.flurgle.blurkit.BlurKitException, NullPointerException { View screenView = view.getRootView(); int width = (int) (crop.width() * downscaleFactor); int height = (int) (crop.height() * downscaleFactor); if (screenView.getWidth() <= 0 || screenView.getHeight() <= 0 || width <= 0 || height <= 0) { throw new com.flurgle.blurkit.BlurKitException("No screen available (width or height = 0)"); } float dx = -crop.left * downscaleFactor; float dy = -crop.top * downscaleFactor; Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); Matrix matrix = new Matrix(); matrix.preScale(downscaleFactor, downscaleFactor); matrix.postTranslate(dx, dy); canvas.setMatrix(matrix); screenView.draw(canvas); return bitmap; } /** * Sets downscale factor to use pre-blur. * See {@link #mDownscaleFactor}. */ public void setDownscaleFactor(float downscaleFactor) { this.mDownscaleFactor = downscaleFactor; // This field is now bad (it's pre-scaled with downscaleFactor so will need to be re-made) this.mLockedBitmap = null; invalidate(); } /** * Sets blur radius to use on downscaled bitmap. * See {@link #mBlurRadius}. */ public void setBlurRadius(int blurRadius) { this.mBlurRadius = blurRadius; // This field is now bad (it's pre-blurred with blurRadius so will need to be re-made) this.mLockedBitmap = null; invalidate(); } /** * Sets FPS to invalidate blur with. * See {@link #mFPS}. */ public void setFPS(int fps) { if (mRunning) { pauseBlur(); } this.mFPS = fps; if (mAttachedToWindow) { startBlur(); } } public void setCornerRadius(float cornerRadius) { this.mCornerRadius = cornerRadius; if (mImageView != null) { mImageView.setCornerRadius(cornerRadius); } invalidate(); } /** * Save the view bitmap to be re-used each frame instead of regenerating. */ public void lockView() { mViewLocked = true; if (mActivityView != null && mActivityView.get() != null) { View view = mActivityView.get().getRootView(); try { setAlpha(0f); mLockedBitmap = getDownscaledBitmapForView(view, new Rect(0, 0, view.getWidth(), view.getHeight()), mDownscaleFactor); setAlpha(1f); mLockedBitmap = com.flurgle.blurkit.BlurKit.getInstance().blur(mLockedBitmap, mBlurRadius); } catch (Exception e) { // ignore } } } /** * Stop using saved view bitmap. View bitmap will now be re-made each frame. */ public void unlockView() { mViewLocked = false; mLockedBitmap = null; } /** * Save the view position to be re-used each frame instead of regenerating. */ public void lockPosition() { mPositionLocked = true; mLockedPoint = getPositionInScreen(); } /** * Stop using saved point. Point will now be re-made each frame. */ public void unlockPosition() { mPositionLocked = false; mLockedPoint = null; } }