Android录屏与截图功能

作者简介  原创微信公众号郭霖 WeChat ID: guolin_blog

本篇来自陈添的投稿,带大家了解5.0新增的屏幕采集接口,用于截屏、录屏,希望对大家有所帮助。

陈添的博客地址:

http://www.jianshu.com/u/f7fa41541bf9

前言

这篇文章,会带你学习如何使用 MediaProjection,MediaCodec 以及 MediaMuxer 来实现简单的截屏和录屏功能。

因为 MediaProjection 是 5.0以上 才出现的,所以今天所讲述功能实现,只在 5.0以上 的系统有效。才疏学浅,讲不深入,见谅。

截屏

步骤如下

1:获取 MediaProjectionManager

2:通过 MediaProjectionManager.createScreenCaptureIntent() 获取 Intent

3:通过 startActivityForResult 传入 Intent 然后在 onActivityResult 中通过 MediaProjectionManager.getMediaProjection(resultCode,data) 获取 MediaProjection

4:创建 ImageReader,构建 VirtualDisplay

5 :最后就是通过 ImageReader 截图,就可以从 ImageReader 里获得 Image 对象。

6 :将 Image 对象转换成 bitmap

实现

步骤已经给出了,我们就按照步骤来实现代码吧。

首先 MediaProjectionManager 是系统服务,我们通过getSystemService(MEDIA_PROJECTION_SERVICE)获取它

projectionManager=(MediaProjectionManager)getSystemService(MEDIA_PROJECTION_SERVICE);

然后调用 startActivityForResult 传入 projectionManager.createScreenCaptureIntent() 创建的 Intent

startActivityForResult(projectionManager.createScreenCaptureIntent(),SCREEN_SHOT);

紧接着我们就可以在 onActivityResult(int requestCode, int resultCode, Intent data) 中通过 resultCode 和 data 来获取 MediaProjection:


然后就是创建 ImageReader 和 VirtualDisplay:


这里我们依次讲解一下:

首先是 ImageReader.newInstance 方法:

publicstaticImageReadernewInstance(intwidth,intheight,intformat,intmaxImages)

方法里接收四个参数。前两个 width,height 是用来指定生成图像的宽和高。

第三个参数format 是图像的格式,这个格式必须是 ImageFormat

或 PixelFormat 中的一个,这两个 Format 里有很多格式,大家可以点进去看看,我们例子中使用的是 PixelFormat.RGBA_8888 格式(需要注意的是并不是所有的格式都被 ImageReader 支持,比如说 ImageFormat.NV21)。

第四个参数maxImages,这个参数指的是你想同时在 ImageReader 里获取到的 Image对象 的个数,这个参数我不是很懂,我不理解同时的意思。我的理解是 ImageReader 是一个类似数组的东西,然后我们可以通过 acquireLatestImage() 或 acquireNextImage() 方法来得到里面的 Image对象(可能有误,仅供参考)。这个值应该设置的越小越好,但是得大于0,所以我们上面设置的是1。

然后我们看看 mediaProjection.createVirtualDisplay 方法:


首先这个方法返回的是 VirtualDisplay。

前四个不用说了,分别是 VirtualDisplay 的名字,宽,高和dpi。

第五个参数,大家可以访问

https://developer.android.google.cn/reference/android/hardware/display/DisplayManager.html

查看 DisplayManager 的所有flags,我没有具体的研究过,在本次要实现的例子里,除了 VIRTUAL_DISPLAY_FLAG_SECURE 这个会报错,其他的 flags 效果都一样。

第六个参数,是一个 Surface。我这里表达一下我的理解,当 VirtualDisplay 被创建出来时,也就是 createVirtualDisplay 调用后,你在真实屏幕上的每一帧都会输入到 Surface参数 里。也就是说,如果你放个 SurfaceView,然后传入 SurfaceView 的 Surface 那么你在屏幕上的操作都会显示在 SurfaceView 里(这里我们后面录屏会讲)。我们这里传入的是 ImageReader 的 Surface。这其中的逻辑我的理解是这样的,真实屏幕的每一帧都都会传给 ImageReader,根据 ImageReader 的 maxImages参数,比如说 maxImages 是2,那么 ImageReader 始终保持两帧图片,但这两帧图片是一直随着真实屏幕的操作而更新的(不知道大家有没有听懂)。

第七个参数,是一个回调函数,在 VirtualDisplay 状态改变时调用。因为我们这里没有,所以传null

第八个参数,这里我给出原文:“The Handler on which the callback should be invoked, or null if the callback should be invoked on the calling thread's main Looper.” 因为我翻译不好。不过和普通的 Handler 使用场景类似。

现在我们 ImageReader 和 VirtualDisplay,接下来我们就可以通过 ImageReader 的 acquireLatestImage() 或 acquireNextImage() 来得到 Image对象 了。

SystemClock.sleep(1000);

Imageimage=imageReader.acquireNextImage();

这里有个坑,就是你在获取 Image 的时候,得先暂停1秒左右,不然就会获取失败(原因未知)。

现在我们有了 Image对象,但是 Image对象 并不能直接作为UI资源被使用,我们可以将它转换成 Bitmap对象。


这里最主要的逻辑就是像素与字节的转换,我们需要将 Image对象 的字节流写进 Bitmap 里,但是 Bitmap 接收的是像素格式的。

我们一行一行来看:

首先获取 image对象 的宽和高,注意 width 和 height 是像素格式的。

然后获取 ByteBuffer,里面存放的就是图片的字节流,是字节格式的。我是这么理解的,ByteBuffer 里面是一长串的字节序列,按照某种格式分成行列就变成了图片。

然后获取 PixelStride,这指的是两个像素的距离(就是一个像素头部到相邻像素的头部),这是字节格式的。

RowStride 是一行占用的距离(就是一行像素头部到相邻行像素的头部),这个大小和 width 有关,这里需要注意,因为内存对齐的原因,所以每行会有一些空余。这个值也是字节格式的。

紧接着我们需要创建一个 Bitmap 用来接受 Image 的 buffer 的输入,buffer 是字节流,它会按照我们设置的 format 转换成像素,所以这里最重要的一个地方就是 Bitmap 创建的大小,因为高度就是行数所以就是 height,但是宽度因为上面说的内存对齐问题会有些空余,所以我们要先求出空余部分,然后加上 width。

introwPadding=rowStride-pixelStride*width;

这句话用整行的距离减去了一行里像素及空隙占用的距离,剩下的就是空余部分。但是这个是字节格式的。我们将它除以 pixelStride,也就是一个像素及空隙占用的字节大小,就转换成了像素格式。然后:

width+rowPadding/pixelStride

这个就是一行里像素的占用了,我们将它传给Bitmap:


创建出合适大小的 Bitmap,然后把 Image 的 buffer 传给它,就成功的将 Image对象 转换成了 Bitmap。这里我可能讲的不清楚,我给大家画了张图:


上面的一小格一小格是一块块像素。

好了,现在我们已经获取到了 bitmap 了,我们可以把它放到 ImageView 里显示一下,我写了一个例子,效果如下:


点击按钮,弹出一个对话框请求截屏,点击立即开始的话,截屏就会显示在下面的 ImageView 里。

截屏就这样,我已经尽力了,╮(╯▽╰)╭

录屏

步骤

录屏的前三步和截屏是一样的,出现分歧点的地方在于 VirtualDisplay 创建时传入的 Surface,还记得我们上面说的吗,说在创建 VirtualDisplay 的时候,传入一个 SurfaceView 的 Surface 的话,那么你在真实屏幕上的操作,都会重现在 SurfaceView 上。我们来试一下:


我们在Surface参数中传入一个 SurfaceView 的 Surface,效果如下:


可以看到我们放了一个 Button,放了一个 ImageView,放了一个 SurfaceView。点击 Button,然后点立即开始之后,真实屏幕就映射到了 SurfaceView 里。

所以当创建 VirtualDisplay 时,真实屏幕就映射到了 Surface,也就是我们可以再 Surface 里拿到屏幕的一个输入。那我们要录屏的话,就只要把 Surface 转换成我们需要的格式就行了。

在本篇文章的例子中,我们会将 Surface对象 转换成 mp4格式。这就需要用到MediaCodec类 和 MediaMuxer类。MediaCodec 生成一个 Surface 用来接收屏幕的输出并按照格式编码,然后传给 MediaMuxer 用来封装成 mp4格式 的视频。


上面讲了 MediaCodec 的创建,我们也可以从中看到屏幕数据是怎么进入 MediaCodec 的。具体的我已经注释了。

接下来我们创建一个 MediaMuxer对象:


然后创建 VirtualDisplay,把 MediaCodec的surface 传进去:


最后就是视频的编码与转换MP4还有保存了:


好了,录屏到此结束了。我们来看下实例演示:


总结

这篇博客写的真是费时费力,果然水平未到就不该强行写文。

我不知道我是不是写清楚了,但还是希望大家看了之后能有一丝丝的收获,这就是对我最大的鼓励。

本篇博客的录屏代码参考自:

https://github.com/yrom/ScreenRecorder

本篇博客的实例代码:

https://github.com/ChenTianSaber/ScreenRecorderShoter

文章原创作者GuoLin 书籍推荐

郭林大神原创android 书籍:《第一行代码 android》

淘宝链接: https://s.click.taobao.com/t?e=m%3D2%26s%3DgKUfuKdAZKocQipKwQzePOeEDrYVVa64K7Vc7tFgwiHjf2vlNIV67p2n%2BQBNMyE6Rku8%2Bpj6eJall3bs%2B3NRhNHnsKI%2BqxhyM0iVZhTFBom4YIorMPnmg8G0g2OJi%2FzmXHfenomYtn5EW9vzeG8LzfPUwktUBEmkxg5p7bh%2BFbQ%3D&pvid=10_106.6.161.154_3367_1490163222155

推荐阅读更多精彩内容