diff --git a/README.md b/README.md index 48fca1e..509f7fe 100644 --- a/README.md +++ b/README.md @@ -34,18 +34,18 @@ dependencies { Parent must be ViewGroup ```kotlin -Blurry.with(context).radius(25).sampling(2).onto(rootView) +Blurry.with(activity).radius(25).sampling(2).onto(rootView) ``` **Into** ```kotlin // from View -Blurry.with(context).capture(view).into(imageView) +Blurry.with(activity).capture(view).into(imageView) ``` ```kotlin // from Bitmap -Blurry.with(context).from(bitmap).into(imageView) +Blurry.with(activity).from(bitmap).into(imageView) ``` **Blur Options** @@ -57,7 +57,7 @@ Blurry.with(context).from(bitmap).into(imageView) - Animation (Overlay Only) ```java -Blurry.with(context) +Blurry.with(activity) .radius(10) .sampling(8) .color(Color.argb(66, 255, 255, 0)) @@ -69,14 +69,14 @@ Blurry.with(context) **Get a bitmap directly** ```kotlin // Sync -val bitmap = Blurry.with(this) +val bitmap = Blurry.with(activity) .radius(10) .sampling(8) .capture(findViewById(R.id.right_bottom)).get() imageView.setImageDrawable(BitmapDrawable(resources, bitmap)) // Async -Blurry.with(this) +Blurry.with(activity) .radius(25) .sampling(4) .color(Color.argb(66, 255, 255, 0)) @@ -86,6 +86,64 @@ Blurry.with(this) } ``` +**Blur a view that contains a Google Map or other surface** + +On API-26 and newer, Blurry uses PixelCopy to copy directly from the surface of the window, +and can thus obtain a bitmap that contains a GoogleMap. Blurry automatically use PixelCopy when using +`Blurry.with(activity)` instead of the deprecated `Blurry.with(context)`. To use get a blurred view for any API, +something like this can be done: + +```kotlin +fun runBlurry() { + // Async to stay off UI thread + Blurry.with(activity) + .sampling(4) // This makes it much faster and more blurry, so less radius is needed, and less radius also makes it faster + .radius(5) + .capture(rootViewContainerToBlur) + .getAsync { + val drawable = BitmapDrawable(target.resources, it) + blurView.setImageDrawable(drawable) + } +} + +if (!Blurry.isSurfaceCaptureSupported) { + // Before API-26, lets help Blurry out and capture the GoogleMap Surface into a regular ImageView, and proceed when ready + blurryMapCaptureView.onMapCaptured { + runBlurry() + } +} else { + runBlurry() +} + +// Let blurryMapCaptureView cover the Google Map Fragment. +val blurryMapCaptureView: BlurryMapCaptureView +class BlurryMapCaptureView : AppCompatImageView { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + private lateinit var map: GoogleMap + + fun initialize(map: GoogleMap) { + this.map = map + } + + fun onMapCaptured(onReadyToCaptureMapAction: () -> Unit) { + map.snapshot { bitmap: Bitmap? -> + visibility = VISIBLE + setImageBitmap(bitmap) + doOnPreDraw { + onReadyToCaptureMapAction() + visibility = GONE + setImageBitmap(null) + } + } + } + +}``` + + + Requirements -------------- Android 5.+ (API 21) diff --git a/blurry/src/main/java/jp/wasabeef/blurry/Blur.java b/blurry/src/main/java/jp/wasabeef/blurry/Blur.java index ed25460..89183ca 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/Blur.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/Blur.java @@ -6,13 +6,21 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.os.Build; import android.renderscript.Allocation; import android.renderscript.Element; import android.renderscript.RSRuntimeException; import android.renderscript.RenderScript; import android.renderscript.ScriptIntrinsicBlur; +import android.util.Log; import android.view.View; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + /** * Copyright (C) 2020 Wasabeef *

@@ -48,9 +56,10 @@ public static Bitmap of(Context context, Bitmap source, BlurFactor factor) { if (Helper.hasZero(width, height)) { return null; } - + // Note that timing has only been added in debug, but app may run slower in debug than in release Profile claims (maybe due to proguard which AndroidBooking does not use) Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + long startDrawToCanvas = System.currentTimeMillis(); Canvas canvas = new Canvas(bitmap); canvas.scale(1 / (float) factor.sampling, 1 / (float) factor.sampling); Paint paint = new Paint(); @@ -59,12 +68,21 @@ public static Bitmap of(Context context, Bitmap source, BlurFactor factor) { new PorterDuffColorFilter(factor.color, PorterDuff.Mode.SRC_ATOP); paint.setColorFilter(filter); canvas.drawBitmap(source, 0, 0, paint); + if (BuildConfig.DEBUG) Log.i("Blurry", "Time to draw source to Canvas: " + (System.currentTimeMillis() - startDrawToCanvas) + "ms"); - try { - bitmap = Blur.rs(context, bitmap, factor.radius); - } catch (RSRuntimeException e) { - bitmap = Blur.stack(bitmap, factor.radius, true); + long startBlur = System.currentTimeMillis(); + if (Build.VERSION.SDK_INT >= 31) { + // Render script is deprecated in Android S + bitmap = Blur.optimizedStack(bitmap, factor.radius, true); + } else { + try { + // RenderScript is hardware accelerated up to Android-11. See https://developer.android.com/guide/topics/renderscript/compute + bitmap = Blur.rs(context, bitmap, factor.radius); + } catch (RSRuntimeException e) { + bitmap = Blur.optimizedStack(bitmap, factor.radius, true); + } } + if (BuildConfig.DEBUG) Log.i("Blurry", "Time to blur: " + (System.currentTimeMillis() - startBlur) + "ms"); if (factor.sampling == BlurFactor.DEFAULT_SAMPLING) { return bitmap; @@ -344,4 +362,309 @@ private static Bitmap stack(Bitmap sentBitmap, int radius, boolean canReuseInBit return (bitmap); } + + public static Bitmap optimizedStack(Bitmap sentBitmap, int radius, boolean canReuseInBitmap) { + + /* + * An optimized version of stack blur, 2x faster than the original. + * + * @author Enrique López Mañas + * http://www.neo-tech.es + * + * Author of the original algorithm: Mario Klingemann + * + * Based heavily on http://vitiy.info/Code/stackblur.cpp + * See http://vitiy.info/stackblur-algorithm-multi-threaded-blur-for-cpp/ + * + * @copyright: Enrique López Mañas + * @license: Apache License 2.0 + */ + + Bitmap bitmap; + if (canReuseInBitmap) { + bitmap = sentBitmap; + } else { + bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); + } + + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + int[] src = new int[w * h]; + bitmap.getPixels(src, 0, w, 0, 0, w, h); + int cores = EXECUTOR_THREADS; + + // The multi-threaded version produces images with small errors, hence disabled + if (cores > 1 && false) { + ExecutorService threadPool = Executors.newFixedThreadPool(cores); // Maybe move out into static Blurry, instead of recreating on each run. + // The cores may not be equally fast, so we make more jobs than cores. Its significantly faster to use cores*5 than cores job on Samsung S20 FE. + int jobs = cores * 10; + List> todo = new ArrayList<>(jobs); + + for (int i = 0; i < jobs; i++) { + final int job = i; + todo.add(() -> { + internal_optimized_stack_iteration(src, w, h, radius, jobs, job, 1); + internal_optimized_stack_iteration(src, w, h, radius, jobs, job, 2); + if (BuildConfig.DEBUG) Log.i("Blurry", job+" finished, thread="+Thread.currentThread().getName()); + return null; + }); + } + try { + threadPool.invokeAll(todo); + } catch (InterruptedException e) { + } + if (BuildConfig.DEBUG) Log.i("Blurry", "All jobs finished"); + } else { + // it runs in same thread, so just say theres 1 core + internal_optimized_stack_iteration(src, w, h, radius, 1, 0, 1); + internal_optimized_stack_iteration(src, w, h, radius, 1, 0, 2); + } + return Bitmap.createBitmap(src, w, h, Bitmap.Config.ARGB_8888); + } + + private static void internal_optimized_stack_iteration(int[] src, int w, int h, int radius, int cores, int core, int step) { + int x, y, xp, yp, i; + int sp; + int stack_start; + int stack_i; + + int src_i; + int dst_i; + + long sum_r, sum_g, sum_b, + sum_in_r, sum_in_g, sum_in_b, + sum_out_r, sum_out_g, sum_out_b; + + int wm = w - 1; + int hm = h - 1; + int div = (radius * 2) + 1; + int mul_sum = stackblur_mul[radius]; + byte shr_sum = stackblur_shr[radius]; + int[] stack = new int[div]; + + if (step == 1) + { + int minY = core * h / cores; + int maxY = (core + 1) * h / cores; + + for(y = minY; y < maxY; y++) + { + sum_r = sum_g = sum_b = + sum_in_r = sum_in_g = sum_in_b = + sum_out_r = sum_out_g = sum_out_b = 0; + + src_i = w * y; // start of line (0,y) + + for(i = 0; i <= radius; i++) + { + stack_i = i; + stack[stack_i] = src[src_i]; + sum_r += ((src[src_i] >>> 16) & 0xff) * (i + 1); + sum_g += ((src[src_i] >>> 8) & 0xff) * (i + 1); + sum_b += (src[src_i] & 0xff) * (i + 1); + sum_out_r += ((src[src_i] >>> 16) & 0xff); + sum_out_g += ((src[src_i] >>> 8) & 0xff); + sum_out_b += (src[src_i] & 0xff); + } + + + for(i = 1; i <= radius; i++) + { + if (i <= wm) src_i += 1; + stack_i = i + radius; + stack[stack_i] = src[src_i]; + sum_r += ((src[src_i] >>> 16) & 0xff) * (radius + 1 - i); + sum_g += ((src[src_i] >>> 8) & 0xff) * (radius + 1 - i); + sum_b += (src[src_i] & 0xff) * (radius + 1 - i); + sum_in_r += ((src[src_i] >>> 16) & 0xff); + sum_in_g += ((src[src_i] >>> 8) & 0xff); + sum_in_b += (src[src_i] & 0xff); + } + + + sp = radius; + xp = radius; + if (xp > wm) xp = wm; + src_i = xp + y * w; // img.pix_ptr(xp, y); + dst_i = y * w; // img.pix_ptr(0, y); + for(x = 0; x < w; x++) + { + src[dst_i] = (int) + ((src[dst_i] & 0xff000000) | + ((((sum_r * mul_sum) >>> shr_sum) & 0xff) << 16) | + ((((sum_g * mul_sum) >>> shr_sum) & 0xff) << 8) | + ((((sum_b * mul_sum) >>> shr_sum) & 0xff))); + dst_i += 1; + + sum_r -= sum_out_r; + sum_g -= sum_out_g; + sum_b -= sum_out_b; + + stack_start = sp + div - radius; + if (stack_start >= div) stack_start -= div; + stack_i = stack_start; + + sum_out_r -= ((stack[stack_i] >>> 16) & 0xff); + sum_out_g -= ((stack[stack_i] >>> 8) & 0xff); + sum_out_b -= (stack[stack_i] & 0xff); + + if(xp < wm) + { + src_i += 1; + ++xp; + } + + stack[stack_i] = src[src_i]; + + sum_in_r += ((src[src_i] >>> 16) & 0xff); + sum_in_g += ((src[src_i] >>> 8) & 0xff); + sum_in_b += (src[src_i] & 0xff); + sum_r += sum_in_r; + sum_g += sum_in_g; + sum_b += sum_in_b; + + ++sp; + if (sp >= div) sp = 0; + stack_i = sp; + + sum_out_r += ((stack[stack_i] >>> 16) & 0xff); + sum_out_g += ((stack[stack_i] >>> 8) & 0xff); + sum_out_b += (stack[stack_i] & 0xff); + sum_in_r -= ((stack[stack_i] >>> 16) & 0xff); + sum_in_g -= ((stack[stack_i] >>> 8) & 0xff); + sum_in_b -= (stack[stack_i] & 0xff); + } + + } + } + // step 2 + else if (step == 2) + { + int minX = core * w / cores; + int maxX = (core + 1) * w / cores; + + for(x = minX; x < maxX; x++) + { + sum_r = sum_g = sum_b = + sum_in_r = sum_in_g = sum_in_b = + sum_out_r = sum_out_g = sum_out_b = 0; + + src_i = x; // x,0 + for(i = 0; i <= radius; i++) + { + stack_i = i; + stack[stack_i] = src[src_i]; + sum_r += ((src[src_i] >>> 16) & 0xff) * (i + 1); + sum_g += ((src[src_i] >>> 8) & 0xff) * (i + 1); + sum_b += (src[src_i] & 0xff) * (i + 1); + sum_out_r += ((src[src_i] >>> 16) & 0xff); + sum_out_g += ((src[src_i] >>> 8) & 0xff); + sum_out_b += (src[src_i] & 0xff); + } + for(i = 1; i <= radius; i++) + { + if(i <= hm) src_i += w; // +stride + + stack_i = i + radius; + stack[stack_i] = src[src_i]; + sum_r += ((src[src_i] >>> 16) & 0xff) * (radius + 1 - i); + sum_g += ((src[src_i] >>> 8) & 0xff) * (radius + 1 - i); + sum_b += (src[src_i] & 0xff) * (radius + 1 - i); + sum_in_r += ((src[src_i] >>> 16) & 0xff); + sum_in_g += ((src[src_i] >>> 8) & 0xff); + sum_in_b += (src[src_i] & 0xff); + } + + sp = radius; + yp = radius; + if (yp > hm) yp = hm; + src_i = x + yp * w; // img.pix_ptr(x, yp); + dst_i = x; // img.pix_ptr(x, 0); + for (y = 0; y < h; y++) { + src[dst_i] = (int) + ((src[dst_i] & 0xff000000) | + ((((sum_r * mul_sum) >>> shr_sum) & 0xff) << 16) | + ((((sum_g * mul_sum) >>> shr_sum) & 0xff) << 8) | + ((((sum_b * mul_sum) >>> shr_sum) & 0xff))); + dst_i += w; + + sum_r -= sum_out_r; + sum_g -= sum_out_g; + sum_b -= sum_out_b; + + stack_start = sp + div - radius; + if (stack_start >= div) stack_start -= div; + stack_i = stack_start; + + sum_out_r -= ((stack[stack_i] >>> 16) & 0xff); + sum_out_g -= ((stack[stack_i] >>> 8) & 0xff); + sum_out_b -= (stack[stack_i] & 0xff); + + if (yp < hm) { + src_i += w; // stride + ++yp; + } + + stack[stack_i] = src[src_i]; + + sum_in_r += ((src[src_i] >>> 16) & 0xff); + sum_in_g += ((src[src_i] >>> 8) & 0xff); + sum_in_b += (src[src_i] & 0xff); + sum_r += sum_in_r; + sum_g += sum_in_g; + sum_b += sum_in_b; + + ++sp; + if (sp >= div) sp = 0; + stack_i = sp; + + sum_out_r += ((stack[stack_i] >>> 16) & 0xff); + sum_out_g += ((stack[stack_i] >>> 8) & 0xff); + sum_out_b += (stack[stack_i] & 0xff); + sum_in_r -= ((stack[stack_i] >>> 16) & 0xff); + sum_in_g -= ((stack[stack_i] >>> 8) & 0xff); + sum_in_b -= (stack[stack_i] & 0xff); + } + } + } + } + + private static final int EXECUTOR_THREADS = Runtime.getRuntime().availableProcessors(); + private static final short[] stackblur_mul = { + 512, 512, 456, 512, 328, 456, 335, 512, 405, 328, 271, 456, 388, 335, 292, 512, + 454, 405, 364, 328, 298, 271, 496, 456, 420, 388, 360, 335, 312, 292, 273, 512, + 482, 454, 428, 405, 383, 364, 345, 328, 312, 298, 284, 271, 259, 496, 475, 456, + 437, 420, 404, 388, 374, 360, 347, 335, 323, 312, 302, 292, 282, 273, 265, 512, + 497, 482, 468, 454, 441, 428, 417, 405, 394, 383, 373, 364, 354, 345, 337, 328, + 320, 312, 305, 298, 291, 284, 278, 271, 265, 259, 507, 496, 485, 475, 465, 456, + 446, 437, 428, 420, 412, 404, 396, 388, 381, 374, 367, 360, 354, 347, 341, 335, + 329, 323, 318, 312, 307, 302, 297, 292, 287, 282, 278, 273, 269, 265, 261, 512, + 505, 497, 489, 482, 475, 468, 461, 454, 447, 441, 435, 428, 422, 417, 411, 405, + 399, 394, 389, 383, 378, 373, 368, 364, 359, 354, 350, 345, 341, 337, 332, 328, + 324, 320, 316, 312, 309, 305, 301, 298, 294, 291, 287, 284, 281, 278, 274, 271, + 268, 265, 262, 259, 257, 507, 501, 496, 491, 485, 480, 475, 470, 465, 460, 456, + 451, 446, 442, 437, 433, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388, + 385, 381, 377, 374, 370, 367, 363, 360, 357, 354, 350, 347, 344, 341, 338, 335, + 332, 329, 326, 323, 320, 318, 315, 312, 310, 307, 304, 302, 299, 297, 294, 292, + 289, 287, 285, 282, 280, 278, 275, 273, 271, 269, 267, 265, 263, 261, 259 + }; + + private static final byte[] stackblur_shr = { + 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, + 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 + }; } diff --git a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java index 94b7b43..8c9efec 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java @@ -1,12 +1,18 @@ package jp.wasabeef.blurry; +import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.Rect; +import android.os.Build; import android.os.Handler; import android.os.Looper; +import android.util.Log; +import android.view.PixelCopy; import android.view.View; +import android.view.Window; -import java.lang.ref.WeakReference; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -32,44 +38,106 @@ public interface Callback { void done(Bitmap bitmap); } - private final WeakReference contextWeakRef; + private static final String TAG = "Blurry_BlurTask"; + private final Context context; private final BlurFactor factor; - private final Bitmap bitmap; + private final Handler handler = new Handler(Looper.getMainLooper()); + private Bitmap bitmap; private final Callback callback; private static final ExecutorService THREAD_POOL = Executors.newCachedThreadPool(); + private boolean usingPixelCopyWithDelayedExecute; - public BlurTask(View target, BlurFactor factor, Callback callback) { + /** + * @param activity Nullable, will fall back to non-surface deprecated drawing-cache when no activity is supplied + */ + public BlurTask(Activity activity /*nullable*/, View target, BlurFactor factor, Callback callback) { this.factor = factor; this.callback = callback; - this.contextWeakRef = new WeakReference<>(target.getContext()); - - target.setDrawingCacheEnabled(true); - target.destroyDrawingCache(); - target.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_LOW); - bitmap = target.getDrawingCache(); + this.context = target.getContext().getApplicationContext(); + // When PixelCopy runs this bitmap is just the reference, its still empty + this.bitmap = extractBitmapWithAsyncPixelCopy(activity, target); } public BlurTask(Context context, Bitmap bitmap, BlurFactor factor, Callback callback) { this.factor = factor; this.callback = callback; - this.contextWeakRef = new WeakReference<>(context); - + this.context = context; this.bitmap = bitmap; } + private Bitmap extractBitmapWithAsyncPixelCopy(Activity activity, View target) { + Bitmap bitmap; + long start = System.currentTimeMillis(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity != null && activity.getWindow().peekDecorView() != null) { + // Use PixelCopy, PixelCopy copies from the surface, and can thus handle GoogleMap + bitmap = Bitmap.createBitmap(target.getWidth(), target.getHeight(), Bitmap.Config.ARGB_8888); + + int[] locations = new int[2]; + target.getLocationInWindow(locations); + Rect rect = new Rect(locations[0], locations[1], locations[0] + target.getWidth(), locations[1] + target.getHeight()); + + Window window = activity.getWindow(); + // See javadoc on PixelCopy.request: https://developer.android.com/reference/android/view/PixelCopy#request(android.view.Window,%20android.graphics.Rect,%20android.graphics.Bitmap,%20android.view.PixelCopy.OnPixelCopyFinishedListener,%20android.os.Handler) + // - it requires that the window's decorView is already defined (handled in if-statement above) + // - and it requires that the window has a backing surface, they recommend postponing till after first onDraw. In this case catch error an fall back to deprecated drawing cache. + // Alternatively we or the user must delay til after first Draw. + try { + PixelCopy.request(window, rect, bitmap, createPixelCopyListener(target), handler); + usingPixelCopyWithDelayedExecute = true; // Must be after successful request, so if surface isn't ready, then the execute() actually executes request + if (Blurry.DO_LOG) Log.d(TAG, "PixelCopy: Registered for callback"); + } catch (IllegalArgumentException e) { + // Handle missing surface. See Android source-code https://github.com/aosp-mirror/platform_frameworks_base/blob/master/graphics/java/android/view/PixelCopy.java + // thus avoid IllegalArgumentException("Window doesn't have a backing surface!") + if (Blurry.DO_LOG) Log.d(TAG, "PixelCopy error (will fallback to manual extraction)", e); + bitmap = extractBitmapByDeprecatedDrawingCache(target); + } + } else { + bitmap = extractBitmapByDeprecatedDrawingCache(target); + } + if (Blurry.DO_LOG) + Log.d(TAG, "Time to extract bitmap: " + (System.currentTimeMillis() - start) + "ms"); // 25-60 ms + return bitmap; + } + + // @android.support.annotation.RequiresApi(api = Build.VERSION_CODES.N) + private PixelCopy.OnPixelCopyFinishedListener createPixelCopyListener(final View target) { + return copyResult -> { + // This runs on main thread, just as we expect the BlurTask constructor to run on + @SuppressLint("InlinedApi") boolean isPixelCopySuccessful = copyResult == PixelCopy.SUCCESS; + if (!isPixelCopySuccessful) { + if (Blurry.DO_LOG) Log.w(TAG, "PixelCopy failed, fallback to manual extraction"); + this.bitmap = extractBitmapByDeprecatedDrawingCache(target); + } else { + if (Blurry.DO_LOG) Log.d(TAG, "PixelCopy success"); + } + executeInnerOnBackgroundThreadPool(); + }; + } + + // This must run on main thread, as it accesses view-methods + private Bitmap extractBitmapByDeprecatedDrawingCache(View target) { + long start = System.currentTimeMillis(); + target.setDrawingCacheEnabled(true); + target.destroyDrawingCache(); + target.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_LOW); + final Bitmap bitmap = target.getDrawingCache(); + if (Blurry.DO_LOG) + Log.d(TAG, "Time to extract bitmap: " + (System.currentTimeMillis() - start) + "ms"); // 25-60 ms + return bitmap; + } + public void execute() { - THREAD_POOL.execute(new Runnable() { - @Override - public void run() { - Context context = contextWeakRef.get(); - if (callback != null) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - callback.done(Blur.of(context, bitmap, factor)); - } - }); - } + if (!usingPixelCopyWithDelayedExecute) { + executeInnerOnBackgroundThreadPool(); + } + } + + private void executeInnerOnBackgroundThreadPool() { + THREAD_POOL.execute(() -> { + // Do the work outside main-thread + Bitmap output = Blur.of(context, bitmap, factor); + if (callback != null) { + handler.post(() -> callback.done(output)); // Run callback on main-thread } }); } diff --git a/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java b/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java index f887eac..1a9567a 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java @@ -1,9 +1,11 @@ package jp.wasabeef.blurry; +import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.os.Build; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; @@ -27,8 +29,21 @@ public class Blurry { private static final String TAG = Blurry.class.getSimpleName(); + public static boolean DO_LOG = BuildConfig.DEBUG; + //@androidx.annotation.ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O) + public static boolean isSurfaceCaptureSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + + /** + * This variant supports PixelCopy for Android API-26 and newer, and supports copying GoogleMaps. + */ + public static Composer with(Activity activity) { + return new Composer(activity); + } + + @Deprecated // ("Use activity variant for API-26 and above") public static Composer with(Context context) { + //noinspection deprecation return new Composer(context); } @@ -42,19 +57,30 @@ public static void delete(ViewGroup target) { public static class Composer { private final View blurredView; + private final Activity activity; // Nullable private final Context context; private final BlurFactor factor; private boolean async; private boolean animate; private int duration = 300; - public Composer(Context context) { + private Composer(Activity activity, Context context) { + this.activity = activity; this.context = context; blurredView = new View(context); blurredView.setTag(TAG); factor = new BlurFactor(); } + public Composer(Activity activity) { + this(activity, activity.getBaseContext()); + } + + @Deprecated // ("Use activity variant for API-26 and above") + public Composer(Context context) { + this(null, context); + } + public Composer radius(int radius) { factor.radius = radius; return this; @@ -87,7 +113,7 @@ public Composer animate(int duration) { } public ImageComposer capture(View capture) { - return new ImageComposer(context, capture, factor, async); + return new ImageComposer(activity, context, capture, factor, async); } public BitmapComposer from(Bitmap bitmap) { @@ -99,13 +125,10 @@ public void onto(final ViewGroup target) { factor.height = target.getMeasuredHeight(); if (async) { - BlurTask task = new BlurTask(target, factor, new BlurTask.Callback() { - @Override - public void done(Bitmap bitmap) { - final BitmapDrawable drawable = - new BitmapDrawable(target.getResources(), Blur.of(context, bitmap, factor)); - addView(target, drawable); - } + BlurTask task = new BlurTask(activity, target, factor, bitmap -> { + final BitmapDrawable drawable = + new BitmapDrawable(target.getResources(), Blur.of(context, bitmap, factor)); + addView(target, drawable); }); task.execute(); } else { @@ -143,12 +166,9 @@ public void into(final ImageView target) { factor.height = bitmap.getHeight(); if (async) { - BlurTask task = new BlurTask(target.getContext(), bitmap, factor, new BlurTask.Callback() { - @Override - public void done(Bitmap bitmap) { - BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); - target.setImageDrawable(drawable); - } + BlurTask task = new BlurTask(target.getContext(), bitmap, factor, bitmap -> { + BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); + target.setImageDrawable(drawable); }); task.execute(); } else { @@ -161,12 +181,14 @@ public void done(Bitmap bitmap) { public static class ImageComposer { + private final Activity activity; private final Context context; private final View capture; private final BlurFactor factor; private final boolean async; - public ImageComposer(Context context, View capture, BlurFactor factor, boolean async) { + public ImageComposer(Activity activity, Context context, View capture, BlurFactor factor, boolean async) { + this.activity = activity; this.context = context; this.capture = capture; this.factor = factor; @@ -178,12 +200,9 @@ public void into(final ImageView target) { factor.height = capture.getMeasuredHeight(); if (async) { - BlurTask task = new BlurTask(capture, factor, new BlurTask.Callback() { - @Override - public void done(Bitmap bitmap) { - BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); - target.setImageDrawable(drawable); - } + BlurTask task = new BlurTask(activity, capture, factor, bitmap -> { + BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap); + target.setImageDrawable(drawable); }); task.execute(); } else { @@ -202,7 +221,7 @@ public Bitmap get() { public void getAsync(BlurTask.Callback callback) { factor.width = capture.getMeasuredWidth(); factor.height = capture.getMeasuredHeight(); - new BlurTask(capture, factor, callback).execute(); + new BlurTask(activity, capture, factor, callback).execute(); } } } diff --git a/build.gradle b/build.gradle index 808b056..5eeea4a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - kotlin_version = '1.4.10' + kotlin_version = '1.7.10' } repositories { @@ -10,7 +10,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.0-beta04' + classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // TODO: Close JCenter on May 1st https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/ diff --git a/example/build.gradle b/example/build.gradle index 8e87ed4..c4657ba 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -36,5 +36,6 @@ repositories { dependencies { implementation project(':blurry') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "androidx.appcompat:appcompat:1.2.0" + implementation "androidx.appcompat:appcompat:1.4.1" + implementation("androidx.core:core-ktx:1.7.0") } diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 23252b5..129350b 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -11,7 +11,8 @@ tools:ignore="GoogleAppIndexingWarning"> + android:label="@string/app_name" + android:exported="true"> diff --git a/example/src/main/java/jp/wasabeef/example/blurry/MainActivity.kt b/example/src/main/java/jp/wasabeef/example/blurry/MainActivity.kt index e8f0912..99d0f7c 100644 --- a/example/src/main/java/jp/wasabeef/example/blurry/MainActivity.kt +++ b/example/src/main/java/jp/wasabeef/example/blurry/MainActivity.kt @@ -7,6 +7,7 @@ import android.util.Log import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import jp.wasabeef.blurry.Blurry @@ -15,6 +16,8 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + Toast.makeText(this, "Touch 'BLURRY' to blur the dogs", Toast.LENGTH_LONG).show() findViewById(R.id.button).setOnClickListener { val startMs = System.currentTimeMillis() diff --git a/gradle.properties b/gradle.properties index a28bed6..fb78740 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,6 @@ android.enableR8.fullMode=true VERSION_NAME=4.0.1 VERSION_CODE=401 -COMPILE_SDK_VERSION=30 -TARGET_SDK_VERSION=30 +COMPILE_SDK_VERSION=31 +TARGET_SDK_VERSION=31 MIN_SDK_VERSION=21 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a2..2e6e589 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists