2121public class FastBlurUtility {
2222
2323 private static final float SCALE_FACTOR = 0.10f ;
24- // Alias public để nơi khác (VD: DialogHelper khi tự chụp screenshot trên main thread rồi
25- // đẩy phần blur sang background thread) biết chụp ở đúng tỉ lệ mà blurCapturedBitmap() cần.
26- public static final float CAPTURE_SCALE = SCALE_FACTOR ;
2724 private static final int BLUR_RADIUS = 8 ;
28- // Bán kính blur GPU tương ứng với ảnh full-res gốc là 20f; vì giờ blur được thực hiện
29- // trên ảnh đã thu nhỏ theo SCALE_FACTOR nên bán kính cũng phải thu nhỏ theo tỉ lệ tương ứng
30- // để giữ hiệu ứng mờ tương đương sau khi phóng to lại (kỹ thuật downsample-blur-upsample).
31- private static final float GPU_BLUR_RADIUS = 20f * SCALE_FACTOR ;
32-
33- // Bảng tra cứu (lookup table) dùng trong StackBlur chỉ phụ thuộc vào bán kính blur.
34- // Vì BLUR_RADIUS luôn là hằng số, bảng này được tính đúng MỘT LẦN khi class được load,
35- // thay vì cấp phát mảng mới (256 * divsum phần tử, ~80KB) và tính lại mỗi lần fastBlur()
36- // chạy - đây từng là một nguồn gây lag/rác GC không cần thiết.
37- private static final int [] DV_LOOKUP = buildLookupTable (BLUR_RADIUS );
38-
39- private static int [] buildLookupTable (int radius ) {
40- int div = radius + radius + 1 ;
41- int divsum = (div + 1 ) >> 1 ;
42- divsum *= divsum ;
43- int [] dv = new int [256 * divsum ];
44- for (int i = 0 ; i < dv .length ; i ++) {
45- dv [i ] = i / divsum ;
46- }
47- return dv ;
48- }
4925
5026 public static Bitmap getBlurBackgroundDrawer (Activity activity ) {
51- if (activity == null || activity .isFinishing ()) return null ;
27+ Bitmap bmp = takeScreenShot (activity );
28+ if (bmp == null ) return null ;
5229
53- View decorView ;
54- int targetW , targetH ;
55- try {
56- decorView = activity .getWindow ().getDecorView ();
57- targetW = decorView .getWidth ();
58- targetH = decorView .getHeight ();
59- } catch (Throwable e ) {
60- return null ;
61- }
62- if (targetW <= 0 || targetH <= 0 ) return null ;
63-
64- // Chụp trực tiếp ở độ phân giải đã thu nhỏ (SCALE_FACTOR) thay vì chụp screenshot
65- // full-res rồi mới scale xuống như trước. Đây là nguồn gây lag lớn nhất trước đây:
66- // - view.draw() vào một Bitmap full màn hình rất tốn bộ nhớ + thời gian rasterize.
67- // - Với path GPU (Android 12+), phải copy toàn bộ hardware buffer kích thước màn
68- // hình gốc từ GPU sang software bitmap (result.copy(ARGB_8888, true)) - rất chậm.
69- // Chụp ảnh nhỏ ngay từ đầu giúp mọi bước xử lý phía sau (vẽ, copy, blur) đều nhanh hơn
70- // nhiều lần vì chỉ phải xử lý ~1% số pixel so với trước.
71- Bitmap smallBmp = takeScreenShot (activity , SCALE_FACTOR );
72- if (smallBmp == null ) return null ;
30+ Bitmap blurredBmp = startBlurBackground (bmp );
7331
74- try {
75- return blurCapturedBitmap (smallBmp , targetW , targetH );
76- } catch (Throwable e ) {
77- // An toàn tuyệt đối: nếu có lỗi toán học hay OOM, không có ảnh để trả về
78- if (!smallBmp .isRecycled ()) smallBmp .recycle ();
79- return null ;
32+ if (bmp != blurredBmp && !bmp .isRecycled ()) {
33+ bmp .recycle ();
8034 }
35+
36+ return blurredBmp ;
8137 }
8238
83- /**
84- * Giữ lại cho tương thích ngược: xử lý blur từ một bitmap full-res có sẵn (ví dụ do nơi
85- * khác tự chụp screenshot rồi truyền vào). Nếu có thể, nên dùng getBlurBackgroundDrawer()
86- * để tận dụng việc chụp ảnh trực tiếp ở kích thước nhỏ.
87- */
8839 public static Bitmap startBlurBackground (Bitmap bkg ) {
8940 if (bkg == null || bkg .isRecycled ()) return null ;
9041
9142 try {
92- int targetW = bkg .getWidth ();
93- int targetH = bkg .getHeight ();
43+ // 1. Android 12+ (API 31+): Dùng GPU RenderEffect
44+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
45+ Bitmap gpuBlurred = blurWithRenderEffect (bkg , 20f );
46+ if (gpuBlurred != null ) {
47+ return applyDimFilter (gpuBlurred );
48+ }
49+ }
9450
95- int width = Math .max (1 , Math .round (targetW * SCALE_FACTOR ));
96- int height = Math .max (1 , Math .round (targetH * SCALE_FACTOR ));
51+ // 2. Android 11 trở xuống (hoặc nếu GPU fail): Dùng StackBlur CPU
52+ int width = Math .round (bkg .getWidth () * SCALE_FACTOR );
53+ int height = Math .round (bkg .getHeight () * SCALE_FACTOR );
9754
98- Bitmap smallBitmap = Bitmap .createScaledBitmap (bkg , width , height , true );
99- if (smallBitmap == null ) return bkg ;
55+ if (width <= 0 || height <= 0 ) return bkg ;
10056
101- return blurCapturedBitmap (smallBitmap , targetW , targetH );
102- } catch (Throwable e ) {
103- // An toàn tuyệt đối: Nếu có bất kỳ lỗi toán học hay OOM nào, trả lại ảnh gốc
104- return bkg ;
105- }
106- }
57+ Bitmap smallBitmap = Bitmap .createScaledBitmap (bkg , width , height , true );
58+ Bitmap blurred = fastBlur (smallBitmap , BLUR_RADIUS );
10759
108- /**
109- * Thực hiện blur trên một bitmap ĐÃ được thu nhỏ sẵn (theo CAPTURE_SCALE), rồi phóng to +
110- * làm tối (dim) về đúng kích thước targetW x targetH. Bitmap đầu vào sẽ bị recycle bởi
111- * hàm này (hoặc bởi các hàm con của nó) sau khi dùng xong.
112- *
113- * Hàm này KHÔNG đụng tới View hierarchy nên an toàn để gọi trên background thread - khác
114- * với takeScreenShot()/takeScreenShot(activity, scale), vốn bắt buộc phải chạy trên main
115- * thread vì cần view.draw(). Cách dùng khuyến khích cho nơi cần tránh block main thread:
116- * 1. Trên main thread: val small = FastBlurUtility.takeScreenShot(activity, FastBlurUtility.CAPTURE_SCALE)
117- * 2. Trên background thread: val blurred = FastBlurUtility.blurCapturedBitmap(small, targetW, targetH)
118- * 3. Post kết quả về main thread để set background.
119- */
120- public static Bitmap blurCapturedBitmap (Bitmap smallBmp , int targetW , int targetH ) {
121- // 1. Android 12+ (API 31+): Dùng GPU RenderEffect trên ảnh nhỏ (rất nhanh)
122- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
123- Bitmap gpuBlurred = blurWithRenderEffect (smallBmp , GPU_BLUR_RADIUS );
124- if (gpuBlurred != null ) {
125- if (!smallBmp .isRecycled ()) {
126- smallBmp .recycle ();
127- }
128- return scaleAndDim (gpuBlurred , targetW , targetH );
60+ if (smallBitmap != null && smallBitmap != blurred && !smallBitmap .isRecycled ()) {
61+ smallBitmap .recycle ();
12962 }
130- }
13163
132- // 2. Android 11 trở xuống (hoặc nếu GPU fail): Dùng StackBlur CPU
133- Bitmap blurred = fastBlur (smallBmp , BLUR_RADIUS );
64+ if (blurred == null ) return bkg ;
13465
135- if (blurred == null ) {
136- // fastBlur thất bại: vẫn cần trả về đúng kích thước, dùng luôn ảnh nhỏ chưa blur
137- return scaleAndDim (smallBmp , targetW , targetH );
66+ return scaleAndDim (blurred , bkg .getWidth (), bkg .getHeight ());
67+ } catch (Throwable e ) {
68+ // An toàn tuyệt đối: Nếu có bất kỳ lỗi toán học hay OOM nào, trả lại ảnh gốc
69+ return bkg ;
13870 }
139-
140- return scaleAndDim (blurred , targetW , targetH );
14171 }
14272
14373 private static Bitmap blurWithRenderEffect (Bitmap input , float radius ) {
@@ -193,38 +123,48 @@ private static Bitmap blurWithRenderEffect(Bitmap input, float radius) {
193123 return null ;
194124 }
195125
196- public static Bitmap takeScreenShot (Activity activity ) {
197- return takeScreenShot (activity , 1f );
126+ private static Bitmap applyDimFilter (Bitmap src ) {
127+ Bitmap output = Bitmap .createBitmap (src .getWidth (), src .getHeight (), Bitmap .Config .ARGB_8888 );
128+ Canvas canvas = new Canvas (output );
129+
130+ Paint paint = new Paint (Paint .FILTER_BITMAP_FLAG | Paint .ANTI_ALIAS_FLAG );
131+ ColorMatrix cm = new ColorMatrix ();
132+ float contrast = 0.80f ;
133+ cm .set (new float []{
134+ contrast , 0 , 0 , 0 , 0 ,
135+ 0 , contrast , 0 , 0 , 0 ,
136+ 0 , 0 , contrast , 0 , 0 ,
137+ 0 , 0 , 0 , 1 , 0 });
138+ paint .setColorFilter (new ColorMatrixColorFilter (cm ));
139+
140+ canvas .drawBitmap (src , 0 , 0 , paint );
141+ if (!src .isRecycled ()) {
142+ src .recycle ();
143+ }
144+ return output ;
198145 }
199146
200- /**
201- * Chụp screenshot của decor view, có thể vẽ trực tiếp ở tỉ lệ thu nhỏ (scale < 1f) để
202- * tránh phải cấp phát + rasterize một Bitmap full màn hình rồi mới scale xuống sau đó.
203- */
204- public static Bitmap takeScreenShot (Activity activity , float scale ) {
147+ public static Bitmap takeScreenShot (Activity activity ) {
205148 if (activity == null || activity .isFinishing ()) return null ;
206149 try {
207150 View view = activity .getWindow ().getDecorView ();
208- int viewWidth = view .getWidth ();
209- int viewHeight = view .getHeight ();
210- if (viewWidth <= 0 || viewHeight <= 0 ) return null ;
211-
212- int width = Math .max (1 , Math .round (viewWidth * scale ));
213- int height = Math .max (1 , Math .round (viewHeight * scale ));
151+ if (view .getWidth () <= 0 || view .getHeight () <= 0 ) return null ;
214152
215- Bitmap bitmap = Bitmap .createBitmap (width , height , Bitmap .Config .ARGB_8888 );
153+ Bitmap bitmap = Bitmap .createBitmap (view . getWidth (), view . getHeight () , Bitmap .Config .ARGB_8888 );
216154 Canvas canvas = new Canvas (bitmap );
217- if (scale != 1f ) {
218- canvas .scale (scale , scale );
219- }
220155 view .draw (canvas );
221156 return bitmap ;
222157 } catch (Exception e ) {
223158 return null ;
224159 }
225160 }
226161
227- private static void applyDimPaint (Paint paint ) {
162+ private static Bitmap scaleAndDim (Bitmap bitmap , int targetW , int targetH ) {
163+ Bitmap output = Bitmap .createBitmap (targetW , targetH , Bitmap .Config .ARGB_8888 );
164+ Canvas canvas = new Canvas (output );
165+
166+ Paint paint = new Paint (Paint .FILTER_BITMAP_FLAG | Paint .ANTI_ALIAS_FLAG );
167+
228168 ColorMatrix cm = new ColorMatrix ();
229169 float contrast = 0.80f ;
230170 cm .set (new float []{
@@ -233,14 +173,6 @@ private static void applyDimPaint(Paint paint) {
233173 0 , 0 , contrast , 0 , 0 ,
234174 0 , 0 , 0 , 1 , 0 });
235175 paint .setColorFilter (new ColorMatrixColorFilter (cm ));
236- }
237-
238- private static Bitmap scaleAndDim (Bitmap bitmap , int targetW , int targetH ) {
239- Bitmap output = Bitmap .createBitmap (targetW , targetH , Bitmap .Config .ARGB_8888 );
240- Canvas canvas = new Canvas (output );
241-
242- Paint paint = new Paint (Paint .FILTER_BITMAP_FLAG | Paint .ANTI_ALIAS_FLAG );
243- applyDimPaint (paint );
244176
245177 Rect src = new Rect (0 , 0 , bitmap .getWidth (), bitmap .getHeight ());
246178 Rect dst = new Rect (0 , 0 , targetW , targetH );
@@ -284,9 +216,12 @@ private static Bitmap fastBlur(Bitmap sentBitmap, int radius) {
284216 int rsum , gsum , bsum , x , y , i , p , yp , yi , yw ;
285217 int [] vmin = new int [Math .max (w , h )];
286218
287- // Dùng bảng tra cứu đã cache sẵn khi radius trùng với BLUR_RADIUS (trường hợp luôn
288- // xảy ra trong thực tế); chỉ build lại nếu có ai đó gọi fastBlur với radius khác.
289- int [] dv = (radius == BLUR_RADIUS ) ? DV_LOOKUP : buildLookupTable (radius );
219+ int divsum = (div + 1 ) >> 1 ;
220+ divsum *= divsum ;
221+ int [] dv = new int [256 * divsum ];
222+ for (i = 0 ; i < 256 * divsum ; i ++) {
223+ dv [i ] = (i / divsum );
224+ }
290225
291226 yw = yi = 0 ;
292227 int [][] stack = new int [div ][3 ];
@@ -391,17 +326,17 @@ private static Bitmap fastBlur(Bitmap sentBitmap, int radius) {
391326 goutsum += sir [1 ];
392327 boutsum += sir [2 ];
393328 }
394- // Kiểm tra giới hạn hàng dựa trên kích thước thực hm * w
329+ // ĐÃ SỬA: Kiểm tra giới hạn hàng dựa trên kích thước thực hm * w
395330 if (yp < (hm * w )) {
396331 yp += w ;
397332 }
398333 }
399334 yi = x ;
400335 stackpointer = radius ;
401336 for (y = 0 ; y < h ; y ++) {
402- pix [yi ] = (0xff000000 & pix [yi ])
403- | (dv [clampIndex (rsum , dv .length )] << 16 )
404- | (dv [clampIndex (gsum , dv .length )] << 8 )
337+ pix [yi ] = (0xff000000 & pix [yi ])
338+ | (dv [clampIndex (rsum , dv .length )] << 16 )
339+ | (dv [clampIndex (gsum , dv .length )] << 8 )
405340 | dv [clampIndex (bsum , dv .length )];
406341
407342 rsum -= routsum ;
0 commit comments