From fc4cf93d6e783ad312ae5d344b40fad160a109b0 Mon Sep 17 00:00:00 2001 From: Kaustubh Patange Date: Tue, 27 Apr 2021 22:05:28 +0530 Subject: [PATCH 01/15] Optimized stack blur implementation & fixes for Android S --- .../main/java/jp/wasabeef/blurry/Blur.java | 299 +++++++++++++++++- 1 file changed, 295 insertions(+), 4 deletions(-) diff --git a/blurry/src/main/java/jp/wasabeef/blurry/Blur.java b/blurry/src/main/java/jp/wasabeef/blurry/Blur.java index becfe12..e163a67 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/Blur.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/Blur.java @@ -61,10 +61,15 @@ public static Bitmap of(Context context, Bitmap source, BlurFactor factor) { paint.setColorFilter(filter); canvas.drawBitmap(source, 0, 0, paint); - try { - bitmap = Blur.rs(context, bitmap, factor.radius); - } catch (RSRuntimeException e) { - bitmap = Blur.stack(bitmap, factor.radius, true); + if (Build.VERSION.SDK_INT >= 31) { + // Render script is deprecated in Android S + bitmap = Blur.optimizedStack(bitmap, factor.radius, true); + } else { + try { + bitmap = Blur.rs(context, bitmap, factor.radius); + } catch (RSRuntimeException e) { + bitmap = Blur.stack(bitmap, factor.radius, true); + } } if (factor.sampling == BlurFactor.DEFAULT_SAMPLING) { @@ -349,4 +354,290 @@ 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; + + for(int i = 0; i < cores; i++) { + internal_optimized_stack_iteration(src, w, h, radius, cores, i, 1); + internal_optimized_stack_iteration(src, w, h, radius, cores, i, 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 + }; } From f7ae9527c8463810da25701d68e34f6cd54de775 Mon Sep 17 00:00:00 2001 From: Kaustubh Patange Date: Tue, 27 Apr 2021 22:07:19 +0530 Subject: [PATCH 02/15] Missed one replacement --- blurry/src/main/java/jp/wasabeef/blurry/Blur.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blurry/src/main/java/jp/wasabeef/blurry/Blur.java b/blurry/src/main/java/jp/wasabeef/blurry/Blur.java index e163a67..0bfcdcc 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/Blur.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/Blur.java @@ -68,7 +68,7 @@ public static Bitmap of(Context context, Bitmap source, BlurFactor factor) { try { bitmap = Blur.rs(context, bitmap, factor.radius); } catch (RSRuntimeException e) { - bitmap = Blur.stack(bitmap, factor.radius, true); + bitmap = Blur.optimizedStack(bitmap, factor.radius, true); } } From a22b56a33a81b739ab7c057c89d5d2d4999b5c98 Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Thu, 10 Mar 2022 18:44:13 +0100 Subject: [PATCH 03/15] Added PixelCopy implementation to extract bitmap from surface of activity for API-26+. It can obtain pixels from GoogleMap view and other surfaces. Async-bugfix: Execute bitmap-generation off main-thread, as required by async. --- .../java/jp/wasabeef/blurry/BlurTask.java | 58 ++++++++++++----- .../main/java/jp/wasabeef/blurry/Blurry.java | 63 ++++++++++++------- 2 files changed, 84 insertions(+), 37 deletions(-) diff --git a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java index 94b7b43..c954a62 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java @@ -1,9 +1,14 @@ package jp.wasabeef.blurry; +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 java.lang.ref.WeakReference; @@ -34,19 +39,48 @@ public interface Callback { private final WeakReference contextWeakRef; private final BlurFactor factor; - private final Bitmap bitmap; + private Bitmap bitmap; private final Callback callback; private static final ExecutorService THREAD_POOL = Executors.newCachedThreadPool(); + private boolean bitmapExtractCompleted = false; - public BlurTask(View target, BlurFactor factor, Callback callback) { + public BlurTask(Activity activity, View target, BlurFactor factor, Callback callback) { this.factor = factor; this.callback = callback; this.contextWeakRef = new WeakReference<>(target.getContext()); + long start = System.currentTimeMillis(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity != 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()); + + + PixelCopy.request(activity.getWindow(), rect, bitmap, copyResult -> { + if (copyResult != PixelCopy.SUCCESS) { + bitmap = extractBitmapByDeprecatedDrawingCache(target); + } + bitmapExtractCompleted = true; + execute(); + }, new Handler(Looper.getMainLooper())); // We will get the callback on the handler, probably don't use main, maybe it has to be main if we want to extract bitmap old fashioned way on error + } else { + bitmap = extractBitmapByDeprecatedDrawingCache(target); + bitmapExtractCompleted = true; + } + if (BuildConfig.DEBUG) Log.i("Blurry", "Time to extract bitmap: " + (System.currentTimeMillis() - start) + "ms"); // 25-60 ms + } + + private Bitmap extractBitmapByDeprecatedDrawingCache(View target) { + long start = System.currentTimeMillis(); target.setDrawingCacheEnabled(true); target.destroyDrawingCache(); target.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_LOW); - bitmap = target.getDrawingCache(); + final Bitmap bitmap = target.getDrawingCache(); + if (BuildConfig.DEBUG) Log.i("Blurry", "Time to extract bitmap: " + (System.currentTimeMillis() - start) + "ms"); // 25-60 ms + return bitmap; } public BlurTask(Context context, Bitmap bitmap, BlurFactor factor, Callback callback) { @@ -58,19 +92,15 @@ public BlurTask(Context context, Bitmap bitmap, BlurFactor factor, Callback call } public void execute() { - THREAD_POOL.execute(new Runnable() { - @Override - public void run() { + if (bitmapExtractCompleted) { + THREAD_POOL.execute(() -> { Context context = contextWeakRef.get(); + // Do the work outside main-thread + Bitmap output = Blur.of(context, bitmap, factor); if (callback != null) { - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - callback.done(Blur.of(context, bitmap, factor)); - } - }); + new Handler(Looper.getMainLooper()).post(() -> callback.done(output)); } - } - }); + }); + } } } diff --git a/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java b/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java index f887eac..b09c970 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; @@ -28,7 +30,18 @@ public class Blurry { private static final String TAG = Blurry.class.getSimpleName(); + 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 +55,30 @@ public static void delete(ViewGroup target) { public static class Composer { private final View blurredView; + private final Activity activity; 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 +111,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 +123,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 +164,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 +179,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 +198,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 +219,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(); } } } From 08bf3473e7085c140b109a4edd42da7af8fddba3 Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Thu, 10 Mar 2022 18:48:49 +0100 Subject: [PATCH 04/15] Added Multithreaded variant, but disabled it as it produces incorrect blurred images --- .../main/java/jp/wasabeef/blurry/Blur.java | 63 ++++++++++++++----- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/blurry/src/main/java/jp/wasabeef/blurry/Blur.java b/blurry/src/main/java/jp/wasabeef/blurry/Blur.java index ceda9fc..f8f088c 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/Blur.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/Blur.java @@ -11,8 +11,15 @@ 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 +55,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,17 +67,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"); + 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; @@ -380,11 +392,32 @@ public static Bitmap optimizedStack(Bitmap sentBitmap, int radius, boolean canRe bitmap.getPixels(src, 0, w, 0, 0, w, h); int cores = EXECUTOR_THREADS; - for(int i = 0; i < cores; i++) { - internal_optimized_stack_iteration(src, w, h, radius, cores, i, 1); - internal_optimized_stack_iteration(src, w, h, radius, cores, i, 2); + // 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); } @@ -546,8 +579,7 @@ else if (step == 2) 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++) - { + for (y = 0; y < h; y++) { src[dst_i] = (int) ((src[dst_i] & 0xff000000) | ((((sum_r * mul_sum) >>> shr_sum) & 0xff) << 16) | @@ -560,15 +592,14 @@ else if (step == 2) sum_b -= sum_out_b; stack_start = sp + div - radius; - if(stack_start >= div) stack_start -= div; + 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) - { + if (yp < hm) { src_i += w; // stride ++yp; } @@ -578,9 +609,9 @@ else if (step == 2) 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; + sum_r += sum_in_r; + sum_g += sum_in_g; + sum_b += sum_in_b; ++sp; if (sp >= div) sp = 0; @@ -589,9 +620,9 @@ else if (step == 2) 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); + sum_in_r -= ((stack[stack_i] >>> 16) & 0xff); + sum_in_g -= ((stack[stack_i] >>> 8) & 0xff); + sum_in_b -= (stack[stack_i] & 0xff); } } } From fd742807383790b77558b9fb0c7587406b7b7e3f Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Fri, 11 Mar 2022 09:24:42 +0100 Subject: [PATCH 05/15] Updated build and gradle to current level --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 808b056..8a35167 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - kotlin_version = '1.4.10' + kotlin_version = '1.6.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.1.2' 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/ From fbee807dfda29cd86c91f234e90ad0e048c85ea7 Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Thu, 10 Mar 2022 23:58:32 +0100 Subject: [PATCH 06/15] Updated compileSdk to 31 --- example/build.gradle | 3 ++- example/src/main/AndroidManifest.xml | 3 ++- gradle.properties | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) 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/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 From d421a108807ffa7fbf37573f98d2a520b03f9989 Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Fri, 11 Mar 2022 09:18:10 +0100 Subject: [PATCH 07/15] Readme for new Surface copy with PixelCopy, as well as old variant copy of GoogleMap --- README.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 6 deletions(-) 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) From 4a8ce2122767405727fbe6457c5453d9a56019a3 Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Fri, 11 Mar 2022 11:11:52 +0100 Subject: [PATCH 08/15] Fix build --- blurry/src/main/java/jp/wasabeef/blurry/Blur.java | 1 + 1 file changed, 1 insertion(+) diff --git a/blurry/src/main/java/jp/wasabeef/blurry/Blur.java b/blurry/src/main/java/jp/wasabeef/blurry/Blur.java index f8f088c..89183ca 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/Blur.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/Blur.java @@ -6,6 +6,7 @@ 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; From 2b4f10496ba76e768b9c1dfbfc297e04da0e0d7c Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Fri, 11 Mar 2022 11:14:22 +0100 Subject: [PATCH 09/15] Example toast --- .../src/main/java/jp/wasabeef/example/blurry/MainActivity.kt | 3 +++ 1 file changed, 3 insertions(+) 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() From 646be56f0ab759c4e4a4a1ed01488998d5230a45 Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Mon, 21 Mar 2022 15:27:12 +0100 Subject: [PATCH 10/15] Avoid crash in Blurry when app window has no backing surface yet, fallback on drawingcache --- .../java/jp/wasabeef/blurry/BlurTask.java | 32 +++++++++++++------ .../main/java/jp/wasabeef/blurry/Blurry.java | 2 +- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java index c954a62..0568cd9 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java @@ -10,6 +10,7 @@ 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; @@ -44,13 +45,16 @@ public interface Callback { private static final ExecutorService THREAD_POOL = Executors.newCachedThreadPool(); private boolean bitmapExtractCompleted = false; + /** + * @param activity Nullable, will fall back to non-surface deprecated drawing-cache when no activity is supplied + */ public BlurTask(Activity activity, View target, BlurFactor factor, Callback callback) { this.factor = factor; this.callback = callback; this.contextWeakRef = new WeakReference<>(target.getContext()); long start = System.currentTimeMillis(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity != null) { + 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); @@ -58,14 +62,24 @@ public BlurTask(Activity activity, View target, BlurFactor factor, Callback call target.getLocationInWindow(locations); Rect rect = new Rect(locations[0], locations[1], locations[0] + target.getWidth(), locations[1] + target.getHeight()); - - PixelCopy.request(activity.getWindow(), rect, bitmap, copyResult -> { - if (copyResult != PixelCopy.SUCCESS) { - bitmap = extractBitmapByDeprecatedDrawingCache(target); - } - bitmapExtractCompleted = true; - execute(); - }, new Handler(Looper.getMainLooper())); // We will get the callback on the handler, probably don't use main, maybe it has to be main if we want to extract bitmap old fashioned way on error + 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, copyResult -> { + if (copyResult != PixelCopy.SUCCESS) { + bitmap = extractBitmapByDeprecatedDrawingCache(target); + } + bitmapExtractCompleted = true; + execute(); + }, new Handler(Looper.getMainLooper())); // We will get the callback on the handler, probably don't use main, maybe it has to be main if we want to extract bitmap old fashioned way on error + } 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!") + bitmap = extractBitmapByDeprecatedDrawingCache(target); + } } else { bitmap = extractBitmapByDeprecatedDrawingCache(target); bitmapExtractCompleted = true; diff --git a/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java b/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java index b09c970..87b15d8 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java @@ -55,7 +55,7 @@ public static void delete(ViewGroup target) { public static class Composer { private final View blurredView; - private final Activity activity; + private final Activity activity; // Nullable private final Context context; private final BlurFactor factor; private boolean async; From a7d52c9661a82ff98b45ec2bf2b7e926740bd6a0 Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Mon, 25 Jul 2022 16:02:18 +0200 Subject: [PATCH 11/15] Fixed BlurTask PixelCopy which failed to call execute() if PixelCopy failed, for instance because surface not ready. Extracted PixelCopy listener variable, so the try-catch is easier to read. --- .../java/jp/wasabeef/blurry/BlurTask.java | 55 ++++++++++++------- .../main/java/jp/wasabeef/blurry/Blurry.java | 2 + 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java index 0568cd9..8e7995e 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java @@ -38,12 +38,14 @@ public interface Callback { void done(Bitmap bitmap); } + private static final String TAG = "Blurry_BlurTask"; private final WeakReference contextWeakRef; private final BlurFactor factor; + 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 bitmapExtractCompleted = false; + private boolean usingPixelCopyWithDelayedExecute; /** * @param activity Nullable, will fall back to non-surface deprecated drawing-cache when no activity is supplied @@ -67,33 +69,41 @@ public BlurTask(Activity activity, View target, BlurFactor factor, Callback call // - 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. + PixelCopy.OnPixelCopyFinishedListener pixelCopyListener = copyResult -> { + // This runs on main thread, just as we expect the BlurTask constructor to run on + boolean isPixelCopySuccessful = copyResult == PixelCopy.SUCCESS; + if (!isPixelCopySuccessful) { + if (Blurry.DO_LOG) Log.w(TAG, "PixelCopy failed, fallback to manual extraction"); + bitmap = extractBitmapByDeprecatedDrawingCache(target); + } else { + if (Blurry.DO_LOG) Log.d(TAG, "PixelCopy success"); + } + executeInnerOnBackgroundThreadPool(); + }; try { - PixelCopy.request(window, rect, bitmap, copyResult -> { - if (copyResult != PixelCopy.SUCCESS) { - bitmap = extractBitmapByDeprecatedDrawingCache(target); - } - bitmapExtractCompleted = true; - execute(); - }, new Handler(Looper.getMainLooper())); // We will get the callback on the handler, probably don't use main, maybe it has to be main if we want to extract bitmap old fashioned way on error + PixelCopy.request(window, rect, bitmap, pixelCopyListener, 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); - bitmapExtractCompleted = true; } - if (BuildConfig.DEBUG) Log.i("Blurry", "Time to extract bitmap: " + (System.currentTimeMillis() - start) + "ms"); // 25-60 ms + if (Blurry.DO_LOG) Log.d(TAG, "Time to extract bitmap: " + (System.currentTimeMillis() - start) + "ms"); // 25-60 ms } + // 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 (BuildConfig.DEBUG) Log.i("Blurry", "Time to extract bitmap: " + (System.currentTimeMillis() - start) + "ms"); // 25-60 ms + if (Blurry.DO_LOG) Log.d(TAG, "Time to extract bitmap: " + (System.currentTimeMillis() - start) + "ms"); // 25-60 ms return bitmap; } @@ -106,15 +116,20 @@ public BlurTask(Context context, Bitmap bitmap, BlurFactor factor, Callback call } public void execute() { - if (bitmapExtractCompleted) { - THREAD_POOL.execute(() -> { - Context context = contextWeakRef.get(); - // Do the work outside main-thread - Bitmap output = Blur.of(context, bitmap, factor); - if (callback != null) { - new Handler(Looper.getMainLooper()).post(() -> callback.done(output)); - } - }); + if (!usingPixelCopyWithDelayedExecute) { + executeInnerOnBackgroundThreadPool(); } } + + private void executeInnerOnBackgroundThreadPool() { + THREAD_POOL.execute(() -> { + Context context = contextWeakRef.get(); + if (context == null) return ; // if contextWeakRef has been destroyed + // 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 87b15d8..1a9567a 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/Blurry.java @@ -29,7 +29,9 @@ 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; /** From b3c366c7df05b7bcb83a1b438cfa45555290ce8a Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Mon, 25 Jul 2022 16:05:14 +0200 Subject: [PATCH 12/15] Upgraded android build tools and kotlin --- build.gradle | 4 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 8a35167..5eeea4a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - kotlin_version = '1.6.10' + kotlin_version = '1.7.10' } repositories { @@ -10,7 +10,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' + 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/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 From 8349639e35ef2cc12facc3c3ba07b3ac694df4ae Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Mon, 25 Jul 2022 16:06:32 +0200 Subject: [PATCH 13/15] BlurTask: Removed usage of weakRef for context, as BlurTask is only used internally in Blurry and does not live longer than the executing method --- .../src/main/java/jp/wasabeef/blurry/BlurTask.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java index 8e7995e..83e399b 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java @@ -12,7 +12,6 @@ import android.view.View; import android.view.Window; -import java.lang.ref.WeakReference; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -39,7 +38,7 @@ public interface Callback { } private static final String TAG = "Blurry_BlurTask"; - private final WeakReference contextWeakRef; + private final Context context; private final BlurFactor factor; private final Handler handler = new Handler(Looper.getMainLooper()); private Bitmap bitmap; @@ -50,10 +49,10 @@ public interface Callback { /** * @param activity Nullable, will fall back to non-surface deprecated drawing-cache when no activity is supplied */ - public BlurTask(Activity activity, View target, BlurFactor factor, Callback callback) { + public BlurTask(Activity activity /*nullable*/, View target, BlurFactor factor, Callback callback) { this.factor = factor; this.callback = callback; - this.contextWeakRef = new WeakReference<>(target.getContext()); + this.context = target.getContext().getApplicationContext(); long start = System.currentTimeMillis(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity != null && activity.getWindow().peekDecorView() != null) { @@ -110,8 +109,7 @@ private Bitmap extractBitmapByDeprecatedDrawingCache(View 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; } @@ -123,8 +121,6 @@ public void execute() { private void executeInnerOnBackgroundThreadPool() { THREAD_POOL.execute(() -> { - Context context = contextWeakRef.get(); - if (context == null) return ; // if contextWeakRef has been destroyed // Do the work outside main-thread Bitmap output = Blur.of(context, bitmap, factor); if (callback != null) { From 7fa909fa9b3499aa0ee772ffcbdcefa157431797 Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Mon, 25 Jul 2022 16:17:12 +0200 Subject: [PATCH 14/15] Rearranged code so both BlurTask constructors are at top, and the methods are smaller. --- .../java/jp/wasabeef/blurry/BlurTask.java | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java index 83e399b..0e7ff7c 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java @@ -1,5 +1,6 @@ package jp.wasabeef.blurry; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.graphics.Bitmap; @@ -42,6 +43,7 @@ public interface Callback { private final BlurFactor factor; private final Handler handler = new Handler(Looper.getMainLooper()); private Bitmap bitmap; + private View target; private final Callback callback; private static final ExecutorService THREAD_POOL = Executors.newCachedThreadPool(); private boolean usingPixelCopyWithDelayedExecute; @@ -53,7 +55,19 @@ public BlurTask(Activity activity /*nullable*/, View target, BlurFactor factor, this.factor = factor; this.callback = callback; 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.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 @@ -68,19 +82,8 @@ public BlurTask(Activity activity /*nullable*/, View target, BlurFactor factor, // - 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. - PixelCopy.OnPixelCopyFinishedListener pixelCopyListener = copyResult -> { - // This runs on main thread, just as we expect the BlurTask constructor to run on - boolean isPixelCopySuccessful = copyResult == PixelCopy.SUCCESS; - if (!isPixelCopySuccessful) { - if (Blurry.DO_LOG) Log.w(TAG, "PixelCopy failed, fallback to manual extraction"); - bitmap = extractBitmapByDeprecatedDrawingCache(target); - } else { - if (Blurry.DO_LOG) Log.d(TAG, "PixelCopy success"); - } - executeInnerOnBackgroundThreadPool(); - }; try { - PixelCopy.request(window, rect, bitmap, pixelCopyListener, handler); + PixelCopy.request(window, rect, bitmap, createPixelCopyListener(), 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) { @@ -92,7 +95,24 @@ public BlurTask(Activity activity /*nullable*/, View target, BlurFactor factor, } else { bitmap = extractBitmapByDeprecatedDrawingCache(target); } - if (Blurry.DO_LOG) Log.d(TAG, "Time to extract bitmap: " + (System.currentTimeMillis() - start) + "ms"); // 25-60 ms + 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() { + 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 @@ -102,17 +122,11 @@ private Bitmap extractBitmapByDeprecatedDrawingCache(View target) { 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 + if (Blurry.DO_LOG) + Log.d(TAG, "Time to extract bitmap: " + (System.currentTimeMillis() - start) + "ms"); // 25-60 ms return bitmap; } - public BlurTask(Context context, Bitmap bitmap, BlurFactor factor, Callback callback) { - this.factor = factor; - this.callback = callback; - this.context = context; - this.bitmap = bitmap; - } - public void execute() { if (!usingPixelCopyWithDelayedExecute) { executeInnerOnBackgroundThreadPool(); From f70ddee255cba30a2b09721b7e7216fd29e5470d Mon Sep 17 00:00:00 2001 From: Alex Rune Berg Date: Tue, 6 Sep 2022 16:25:05 +0200 Subject: [PATCH 15/15] Fixed nullpointer crash in Blur-view on fallback from PixelCopy --- blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java index 0e7ff7c..8c9efec 100644 --- a/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java +++ b/blurry/src/main/java/jp/wasabeef/blurry/BlurTask.java @@ -43,7 +43,6 @@ public interface Callback { private final BlurFactor factor; private final Handler handler = new Handler(Looper.getMainLooper()); private Bitmap bitmap; - private View target; private final Callback callback; private static final ExecutorService THREAD_POOL = Executors.newCachedThreadPool(); private boolean usingPixelCopyWithDelayedExecute; @@ -83,7 +82,7 @@ private Bitmap extractBitmapWithAsyncPixelCopy(Activity activity, View target) { // - 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(), handler); + 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) { @@ -101,7 +100,7 @@ private Bitmap extractBitmapWithAsyncPixelCopy(Activity activity, View target) { } // @android.support.annotation.RequiresApi(api = Build.VERSION_CODES.N) - private PixelCopy.OnPixelCopyFinishedListener createPixelCopyListener() { + 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;