Android进阶之路 - 花样百出的截屏需求

Posted Modu_MrLiu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android进阶之路 - 花样百出的截屏需求相关的知识,希望对你有一定的参考价值。

最近又遇到了截图相关的需求,联合之前的截图需求后,抽时间整理了一下截屏的常见需求,特此记录 ~

关联篇

针对每个人不同的需求,可采用不同的方法 - - ~

截取当前屏幕界面(activity)

一般调用对应方法时出现调用不到的场景时,我们需要根据提示进行修改,这里应该需要通过activity进行方法调用 ~

别人的方法

    public Bitmap screen() {
        View dView = getWindow().getDecorView();
        dView.setDrawingCacheEnabled(true);
        dView.buildDrawingCache();
        return Bitmap.createBitmap(dView.getDrawingCache());
    }

我的方法 - activity截图

    //缺点:无法截取WebView页面,截屏后是白屏!
    public static Bitmap capture(Activity activity) {
        activity.getWindow().getDecorView().setDrawingCacheEnabled(true);
        Bitmap bmp = activity.getWindow().getDecorView().getDrawingCache();
        return bmp;
    }

截图并保存

    public void screen() {
        View dView = getWindow().getDecorView();
        dView.setDrawingCacheEnabled(true);
        dView.buildDrawingCache();
        Bitmap bitmap = Bitmap.createBitmap(dView.getDrawingCache());
        if(bitmap !=null ) {
            try {
                // 获取内置SD卡路径
                String sdCardPath = Environment.getExternalStorageDirectory().getPath();
                // 图片文件路径(自行定义)
                String filePath = sdCardPath + File.separator +"screenshot.png";
                File file =new File(filePath);
                FileOutputStream os = new FileOutputStream(file);
                bitmap.compress(Bitmap.CompressFormat.PNG,100, os);
                os.flush();
                os.close();
            } catch(Exception e) {
            }
        }
    }

截取某个控件或区域(view)

可作用于根view或某个控件view

  • 简洁版
	/**
     * 截取指定View为图片
     */
    public static Bitmap captureView(View view) throws Throwable {
        Bitmap bm = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
        view.draw(new Canvas(bm));
        return bm;
    }

截图并保存

  public void captureView(View view) {
  		//图片存储路径,可以不关注
        String imgPath = "/sdcard/test.png";
        view.setDrawingCacheEnabled(true);
        view.buildDrawingCache();
        Bitmap bitmap = view.getDrawingCache();
        //图片存储部分可以不关注
        if (bitmap != null) {
            try {
                FileOutputStream out = new FileOutputStream(imgPath);
                bitmap.compress(Bitmap.CompressFormat.PNG, 100,
                        out);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
  • 繁琐版
   public void screen(View view){
        View dView = view;
        dView.setDrawingCacheEnabled(true);
        dView.buildDrawingCache();
        //以下俩种方式,一种是取图片缓存,一种是重新绘制,可自行调节尝试
        //Bitmap bitmap = Bitmap.createBitmap(dView.getDrawingCache());
        Bitmap bitmap = Bitmap.createBitmap(dView.getWidth(), dView.getHeight(), Bitmap.Config.ARGB_8888);
        //使用Canvas,调用自定义view控件的onDraw方法,绘制图片
        Canvas canvas =new Canvas(bitmap);
        dView.draw(canvas);
    }
  • view截图(有人说截取view转bitmap后为null,可用这种方式清理缓存,未尝试
    public static Bitmap getViewBp(View v) {
        if (null == v) {
            return null;
        }
        v.setDrawingCacheEnabled(true);
        v.buildDrawingCache();
        if (Build.VERSION.SDK_INT >= 11) {
            v.measure(View.MeasureSpec.makeMeasureSpec(v.getWidth(),
                    View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(
                    v.getHeight(), View.MeasureSpec.EXACTLY));
            v.layout((int) v.getX(), (int) v.getY(),
                    (int) v.getX() + v.getMeasuredWidth(),
                    (int) v.getY() + v.getMeasuredHeight());
        } else {
            v.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
            v.layout(0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());
        }
        Bitmap b = Bitmap.createBitmap(v.getDrawingCache(), 0, 0, v.getMeasuredWidth(), v.getMeasuredHeight());

        v.setDrawingCacheEnabled(false);
        v.destroyDrawingCache();
        return b;
    }
  • view截图(有人说截取view转bitmap后为null,可用这种方式清理缓存,未尝试
	public static Bitmap convertViewToBitmap(View view) {
        view.setDrawingCacheEnabled(true);
        view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
        view.buildDrawingCache();
        Bitmap bitmap = view.getDrawingCache();
        view.setDrawingCacheEnabled(false);
        return bitmap;
 	}

截取长屏

​ 截取长屏其实原理就是截取整个ScrollView或者ListView的视图,因此实现原理跟上面中提到的截取某个控件的View基本一致。

ScrollView 实现截屏
		/*
         * 截取scrollview的屏幕
         **/
     public static Bitmap getScrollViewBitmap(ScrollView scrollView) {
         int h = 0;
         Bitmap bitmap;
         for  ( int i = 0 ; i < scrollView.getChildCount(); i++) {
         		h += scrollView.getChildAt(i).getHeight();
   		 }
 
         // 创建对应大小的bitmap
         bitmap = Bitmap.createBitmap(scrollView.getWidth(), h,Bitmap.Config.ARGB_8888);
         final Canvas canvas = new Canvas(bitmap);
         scrollView.draw(canvas);  
         return bitmap;
      }
ListView实现截屏
   /**
     * 截图listview
     **/
     public static Bitmap getListViewBitmap(ListView listView) {
         int h = 0;
         Bitmap bitmap;
         // 获取listView实际高度
         for  (int  i = 0 ; i < listView.getChildCount(); i++) {
        	 h += listView.getChildAt(i).getHeight();
       	 }
         Log.d(TAG,   "实际高度:"  + h);  
         Log.d(TAG,   "list 高度:"  + listView.getHeight());
         
         // 创建对应大小的bitmap
         bitmap = Bitmap.createBitmap(listView.getWidth(), h,       
         Bitmap.Config.ARGB_8888); 
         final  Canvas canvas =  new Canvas(bitmap);
         listView.draw(canvas);    
         return bitmap;
    }

WebView实现截屏

    //这是webview的,利用了webview的api
    private static Bitmap captureWebView(WebView webView) {
         Picture snapShot = webView.capturePicture();
         Bitmap bmp = Bitmap.createBitmap(snapShot.getWidth(),     
         snapShot.getHeight(), Bitmap.Config.ARGB_8888);   
         Canvas canvas = new Canvas(bmp);
         snapShot.draw(canvas);    
         return bmp;  
    }

局部、全局实时截屏

关于应用内全局截屏的需求,我在 如何优雅的实时获取用户操作界面 中详细的解释了使用方式


在日常开发中,一般截屏后的图片,我们都会进行压缩、保存,这里一并进行记录

图片压缩

 /**
     * 压缩图片
     *
     * @param bgimage
     * @param newWidth
     * @param newHeight
     * @return
     */
    public static Bitmap zoomImage(Bitmap bgimage, double newWidth, double newHeight) {
        // 获取这个图片的宽和高
        float width = bgimage.getWidth();
        float height = bgimage.getHeight();
        // 创建操作图片用的matrix对象
        Matrix matrix = new Matrix();
        // 计算宽高缩放率
        float scaleWidth = ((float) newWidth) / width;
        float scaleHeight = ((float) newHeight) / height;
        // 缩放图片动作
        //matrix.postScale(scaleWidth, scaleHeight);//TODO 因为宽高不确定的因素,所以不缩放
        Bitmap bitmap = Bitmap.createBitmap(bgimage, 0, 0, (int) width,
                (int) height, matrix, true);
        return bitmap;
    }

截图、压缩

	 	Bitmap bitmap = null;
          try {
                 bitmap = captureView(mShareBackgroundSign);
              } catch (Throwable throwable) {
                    throwable.printStackTrace();
              }
           //图片压缩,加快使用速度~
           zoomImage(bitmap, 720, 1280);

保存图片到本地

注意:切记适配6.0、7.0

public static void savePhotoToSDCard(Bitmap photoBitmap, String path, String photoName) {
        if (checkSDCardAvailable()) {
            File dir = new File(path);
            if (!dir.exists()) {
                dir.mkdirs();
            }
 
            File photoFile = new File(path, photoName + ".png");
            FileOutputStream fileOutputStream = null;
            try {
                fileOutputStream = new FileOutputStream(photoFile);
                if (photoBitmap != null) {
                    if (photoBitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)) {
                        fileOutputStream.flush();
                    }
                }
            } catch (FileNotFoundException e) {
                photoFile.delete();
                e.printStackTrace();
            } catch (IOException e) {
                photoFile.delete();
                e.printStackTrace();
            } finally {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

截屏扩展

以下部分其实我并未在有限的开发中使用到,仅当做个人兴趣的知识延伸

截取带导航栏的整个屏幕

采用adb命令方式截屏的操作,屈指可数,可当做知识扩展,没有必要深读

  • 优点:只需在代码中执行截屏的命令即可,可根据需求封装一个方法,传入保存的路径和文件名即可;
  • 缺点:手机需要获取ROOT权限,调用命令之前需要先请求su获取ROOT权限;

adb 命令:这里指的不是连接电脑进行adb操控,而是在App内部实现adb命令的操控

在APK中调用 "adb shell screencap -p filepath" 命令

注意:该命令读取系统的framebuffer,需要获得系统权限

  1. androidManifest.xml文件中添加
 <uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
  1. 修改APK为系统权限,将APK放到源码中编译, 修改Android.mk
 LOCAL_CERTIFICATE:= platform
        public void takeScreenShot () {
            String mSavedPath = Environment.getExternalStorageDirectory() + File.separator + "screenshot.png";
            try {
                Runtime.getRuntime().exec("screencap -p " + mSavedPath);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

利用系统的隐藏API,实现Screenshot,这部分代码是系统隐藏的,需要在源码下编译。

  1. 修改Android.mk, 添加系统权限
 LOCAL_CERTIFICATE := platform
  1. 修改AndroidManifest.xml 文件,添加权限
 <uses-permission android:name="android.permission.READ_FRAME_BUFFER" />

Android本地编程(Native Programming)读取framebuffer命令行,框架的截屏功能是通过framebuffer来实现的

framebuffer介绍

帧缓冲(framebuffer)是Linux为显示设备提供的一个接口,把显存抽象后的一种设备,他允许上层应用程序在图形模式下直接对显示缓冲区进行 读写操作。这种操作是抽象的,统一的。用户不必关心物理显存的位置、换页机制等等具体细节。这些都是由Framebuffer设备驱动来完成的。
linux FrameBuffer 本质上只是提供了对图形设备的硬件抽象,在开发者看来,FrameBuffer 是一块显示缓存,往显示缓存中写入特定格式的数据就意味着向屏幕输出内容。所以说FrameBuffer就是一块白板。例如对于初始化为16 位色的FrameBuffer 来说, FrameBuffer中的两个字节代表屏幕上一个点,从上到下,从左至右,屏幕位置与内存地址是顺序的线性关系。
帧缓存有个地址,是在内存里。我们通过不停的向frame buffer中写入数据, 显示控制器就自动的从frame buffer中取数据并显示出来。全部的图形都共享内存中同一个帧缓存。

android截屏实现思路

Android系统是基于Linux内核的,所以也存在framebuffer这个设备,我们要实现截屏的话只要能获取到framebuffer中的数据,然后把数据转换成图片就可以了,android中的framebuffer数据是存放在 /dev/graphics/fb0 文件中的,所以我们只需要来获取这个文件的数据就可以得到当前屏幕的内容。
现在我们的测试代码运行时候是通过RC(remote controller)方式来运行被测应用的,那就需要在PC机上来访问模拟器或者真机上的framebuffer数据,这个的话可以通过android的ADB命令来实现。
各大手机自带的按键组合进行截屏,Android源码中对按键的捕获位于文件PhoneWindowManager.java(alps\\frameworks\\base\\policy\\src\\com\\android\\internal\\policy\\impl)中,这个类处理所有的键盘输入事件,其中函数interceptKeyBeforeQueueing()会对常用的按键做特殊处理。

截取非含当前应用的屏幕部分(最佳官方方案)

Android 在5.0 之后支持了实时录屏的功能;通过实时录屏我们可以拿到截屏的图像。同时可以通过在Service中处理实现后台的录屏(具体的类讲解大家自行网上查阅)

类似使用手机的系统截屏(音量下键+电源键),针对的并非一个view,而是整个屏幕,在调用这个服务的时候,会弹出一个权限确认的弹框;同时需注意,这一方法只能在Android 5.0的系统设备上使用。

详细步骤

  1. 初始化一个MediaProjectionManager
 MediaProjectionManager mMediaProjectionManager = (MediaProjectionManager)getApplication().getSystemService(Context.MEDIA_PROJECTION_SERVICE); 
  1. 创建intent,并启动Intent(注意:这里是startActivityForResult)
  2. 以上是关于Android进阶之路 - 花样百出的截屏需求的主要内容,如果未能解决你的问题,请参考以下文章

    Android自己定义截屏功能,相似QQ截屏

    android源码解析(二十六)-->截屏事件流程

    iOS中的截屏(屏幕截屏及scrollView或tableView的全部截屏)

    一个基于Qt的截屏程序

    Qt学习: QPixmap实现的截屏程序代码示例

    Python中的截屏模块 pyscreenshot