Android 图片模糊及bitmap相关

Android 图片模糊及bitmap相关

简介

任务:window弹窗的背景模糊

实现原理:在window弹窗内容显示出来之前先截取手机屏幕,然后模糊,等弹窗出来之后根据弹窗大小自适应截图,设置为背景,从而实现弹窗背景模糊的效果

难点:

  1. 由于我们的应用没有activity常驻,而且是系统应用,所以和普通应用的截屏不同;
  2. 使用反射的方法调用系统api进行截屏时,获取屏幕高度有个坑,需要特别注意;
  3. 直接使用原生api进行模糊,radius设置为最大25并不能满足业务要求,所以还另外需要对图片进行处理。

截屏

截屏的方法有很多种,这里介绍常用的几种:

  1. adb命令截屏

    adb命令截屏需要root权限,而且由于涉及io,所以比较耗时

       private Bitmap getScreenByAdb(String localImgPath) {
    //       adb命令获取屏幕截图,与实际屏幕尺寸相同,但是涉及IO所以耗时
           Process process = null;
           try {
               process = Runtime.getRuntime().exec("screencap " +  localImgPath);
               process.waitFor();//比较耗时,尤其屏幕色彩比较复杂时,耗时甚至会到3s
          } catch (Exception e) {
               e.printStackTrace();
          } finally {
               if (process != null) {
                   process.destroy();
              }
          }
           FileInputStream fis = null;
           try {
               fis = new FileInputStream(localImgPath);
          } catch (FileNotFoundException e) {
               e.printStackTrace();
          }
           Bitmap originalBgBitmap = BitmapFactory.decodeStream(fis);
           return originalBgBitmap;
      }
    
  2. 反射调用系统api截屏

    使用该方法截屏时需要是系统应用

       private Bitmap getScreenBySysMethod() {
           DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
           Bitmap originalBgBitmap = null;
           try {
               Class<?> mClassType = Class.forName("android.view.SurfaceControl");
               Method nativeScreenshotMethod = mClassType.getDeclaredMethod("screenshot", Rect.class, int.class, int.class, int.class);
               nativeScreenshotMethod.setAccessible(true);
               Bitmap sBitmap = (Bitmap) nativeScreenshotMethod.invoke(mClassType, new Rect(), dm.widthPixels,
                       dm.heightPixels, Surface.ROTATION_0);
               originalBgBitmap = sBitmap.copy(Bitmap.Config.ARGB_8888, true);
          } catch (NoSuchMethodException e) {
               e.printStackTrace();
          } catch (ClassNotFoundException e) {
               e.printStackTrace();
          } catch (InvocationTargetException e) {
               e.printStackTrace();
          } catch (IllegalAccessException e) {
               e.printStackTrace();
          }
    ​
           return originalBgBitmap;
      }
    

    在使用这个方法截屏时有个坑,当时折腾了我挺长时间,就是在部分全面屏手机上直接使用dm.heightPixels获取的屏幕高度不包含状态栏,看dm.heightPixels的源码,注释写的是The absolute height of the available display size in pixels,所以不包含状态栏也对。这时通过另外一种方式可以获取屏幕高度:

      /**
        * 在activity中获取屏幕的真实高度,由于在部分全面屏手机上,
        * 直接使用DisplayMetrics heightPixels获取屏幕高度没有包含状态栏或者虚拟按键
        * 所以会比实际的小几十个像素
        * @param context
        * @return
        */
       public static int getScreenHeight(Activity context) {
           DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
           context.getWindowManager().getDefaultDisplay().getRealMetrics(displayMetrics);
           return displayMetrics.heightPixels;
      }
    
  3. 普通应用截屏

    这个方法一般用于activity或者fragment中调用,因为需要调用getWindow().getDecorView()获取根布局。所以,如果在需要截屏的时候,如果没有activity活动,是无法使用这个方法的。

       private Bitmap getScreenshotView() {
           View view = getWindow().getDecorView();
           view.setDrawingCacheEnabled(true); // 设置缓存,可用于实时截图
           Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
           Canvas canvas = new Canvas(bitmap);
           view.draw(canvas);
           view.setDrawingCacheEnabled(false); // 清空缓存,可用于实时截图
           return bitmap;
      }
    

    同时,由于这个方法在获取截屏时是通过view进行的,所以也可以只对某个view进行局部截图,只要将方法中的view对象换成需要截图的view就可以了。

Bitmap模糊

GitHub上有现成的模糊控件,可以满足很多场景,比如github.com/mmin18/Real…等。可惜不满足我的需求,我这边只需要对拿到的bitmap进行模糊,直接使用Android原生的就可以,代码如下:

/**
* 
* @param context 上下文
* @param src 原图
* @param radius 模糊半径,有效范围0-25
* @return
*/
private Bitmap blur(Context context, Bitmap src, float radius) {
  RenderScript rs = RenderScript.create(context);
       final Allocation input = Allocation.createFromBitmap(rs, src);
       final Allocation output = Allocation.createTyped(rs, input.getType());
       final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
       script.setRadius(radius);
       script.setInput(input);
       script.forEach(output);
       output.copyTo(src);
       return src;
}

如果这个方法模糊度不够的话,还可以这样来做:先将bitmap缩小,再进行模糊,然后再放大,这样得到的模糊度一般是可以满足需求的。

缩小bitmap

BitmapTransformUtil.scaleBitmap(originalBgBitmap, 0.2f, 0.2f, true);

放大bitmap

BitmapTransformUtil.scaleBitmap(blurredBgBitmap, 5.0f, 5.0f, true);

其中scaleBitmap方法如下所示:

   /**
    * 根据指定的宽度比例值和高度比例值进行缩放
    *
    * @param srcBitmap
    * @param scaleWidth
    * @param scaleHeight
    * @param recycleSrc 是否回收Bitmap
    * @return
    */
   public static Bitmap scaleBitmap(Bitmap srcBitmap, float scaleWidth, float scaleHeight, boolean recycleSrc) {
       int width = srcBitmap.getWidth();
       int height = srcBitmap.getHeight();
       Matrix matrix = new Matrix();
       matrix.postScale(scaleWidth, scaleHeight);
       Bitmap bitmap = Bitmap.createBitmap(srcBitmap, 0, 0, width, height, matrix, true);
       if (bitmap != null) {
           /**回收*/
           if (recycleSrc && srcBitmap != null && !srcBitmap.equals(bitmap) && !srcBitmap.isRecycled()) {
               GlideBitmapPool.putBitmap(srcBitmap);
          }
           return bitmap;
      } else {
           return srcBitmap;
      }
  }

Bitmap截取

拿到整个手机屏幕的截图后,需要借去部分作为背景,截取bitmap方法如下:

   /**
    * 裁剪一定高度保留上半部分
    *
    * @param srcBitmap 原图
    * @param x         起始坐标x
    * @param y         起始坐标y
    * @param width     目标宽度
    * @param height     目标高度
    * @param recycleSrc 是否回收原图
    * @return
    */
   public static Bitmap cropBitmapTop(Bitmap srcBitmap, int x, int y, int width, int height, boolean recycleSrc) {
​
       /**裁剪关键步骤*/
       Bitmap cropBitmap = Bitmap.createBitmap(srcBitmap, x, y, width, height);
​
       /**回收之前的Bitmap*/
       if (recycleSrc && srcBitmap != null && !srcBitmap.equals(cropBitmap) && !srcBitmap.isRecycled()) {
           GlideBitmapPool.putBitmap(srcBitmap);
      }
       return cropBitmap;
  }

Bitmap圆角设置

   /**
    * 设置圆角
    *
    * @param srcBitmap
    * @param recycleSrc 是否回收原图
    * @return
    */
   public static Bitmap getRoundedCornerBitmap(Bitmap srcBitmap, float radius, boolean recycleSrc) {
       Bitmap output = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), Bitmap.Config.ARGB_8888);
       Paint paint = new Paint();
       Rect rect = new Rect(0, 0, srcBitmap.getWidth(), srcBitmap.getHeight());
       RectF rectF = new RectF(rect);
       paint.setAntiAlias(true);
       Canvas canvas = new Canvas(output);
       canvas.save();
       canvas.drawARGB(0, 0, 0, 0);
       canvas.drawRoundRect(rectF, radius, radius, paint);
       paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
       canvas.drawBitmap(srcBitmap, rect, rect, paint);
       canvas.restore();
       /**回收之前的Bitmap*/
       if (recycleSrc && srcBitmap != null && !srcBitmap.equals(output) && !srcBitmap.isRecycled()) {
           GlideBitmapPool.putBitmap(srcBitmap);
      }
       return output;
  }

Bitmap保存

    /**
    * 保存bitmap到本地
    * @param bitmap 
    * @param filePath 
    */
   private void saveBitmap(Bitmap bitmap, String filePath){
       FileOutputStream fos = null;
       try {
           fos = new FileOutputStream(filePath);
           bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
           fos.flush();
      } catch (IOException e) {
           e.printStackTrace();
      } finally {
           try {
               if (fos != null) {
                   fos.close();
              }
          } catch (IOException e) {
               e.printStackTrace();
          }
      }
  }

推荐一个bitmap复用池:github.com/amitshekhar…,毕竟bitmap是内存消耗大户,合理使用bitmap可以避免内存抖动和OOM。具体使用方法直接点进去看吧。

最后贴一个实现效果图吧

image.png

猜你喜欢

转载自juejin.im/post/7116432182448488479