详解有关Android截图与录屏功能的学习

简单的截屏和录屏功能。

因为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

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if(requestCode == SCREEN_SHOT){
      if(resultCode == RESULT_OK){
        //获取MediaProjection
        mediaProjection = projectionManager.getMediaProjection(requestCode,data);
      }
    }
  }

然后就是创建ImageReader和VirtualDisplay

    imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1);
    if(imageReader!=null){
      Log.d(TAG, "imageReader Successful");
    }
    mediaProjection.createVirtualDisplay("ScreenShout",
        width,height,dpi,
        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
        imageReader.getSurface(),null,null);

这里我们依次讲解一下。

首先是ImageReader.newInstance方法:

代码如下:

public static ImageReader newInstance(int width, int height, int format, int maxImages)

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

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

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

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

createVirtualDisplay(@NonNull String name,
      int width, int height, int dpi, int flags, @Nullable Surface surface,
      @Nullable VirtualDisplay.Callback callback, @Nullable Handler handler)

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

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

第五个参数,大家可以点 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);
Image image = imageReader.acquireNextImage();

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

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

    int width = image.getWidth();
    int height = image.getHeight();
    final Image.Plane[] planes = image.getPlanes();
    final ByteBuffer buffer = planes[0].getBuffer();
    int pixelStride = planes[0].getPixelStride();
    int rowStride = planes[0].getRowStride();
    int rowPadding = rowStride - pixelStride * width;
    bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);
    bitmap.copyPixelsFromBuffer(buffer);
    image.close();

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

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

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

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

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

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

int rowPadding = rowStride - pixelStride * width;

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

width + rowPadding / pixelStride

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

代码如下:

bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);

创建出合适大小的Bitmap,然后把Image的buffer传给它,就成功的将Image对象转换成了Bitmap。

这里我可能讲的不清楚,我给大家画了张图:

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

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

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

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

录屏:

步骤:

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

mediaProjection.createVirtualDisplay("ScreenShout",
        width,height,dpi,
        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
        surfaceView.getHolder().getSurface(),null,null);

我们在Surface参数中传入一个SurfaceView的Surface

效果如下:

可以看到我们放了一个Button,放了一个ImageView,放了一个SurfaceView。

点击Button,然后点立即开始之后,真实屏幕就映射到了SurfaceView里。

所以当创建VirtualDisplay时,真实屏幕就映射到了Surface,也就是我们可以再Surface里拿到屏幕的一个输入。那我们要录屏的话,就只要把Surface转换成我们需要的格式就行了,在本篇文章的例子中,我们会将Surface对象转换成mp4格式。这就需要用到MediaCodec类和MediaMuxer类。MediaCodec生成一个Surface用来接收屏幕的输出并按照格式编码,然后传给MediaMuxer用来封装成mp4格式的视频。

    //第一个参数是mime类型,我们传入video/avc
    //第二第三个参数是宽和高
    MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
    //COLOR_FormatSurface这里表明数据将是一个graphicbuffer元数据
    format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
        MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
    //设置码率,码率越大视频越清晰,相对的占用内存也要更大
    format.setInteger(MediaFormat.KEY_BIT_RATE, 6000000);
    //设置帧数
    format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
    //设置两个关键帧的间隔,这个值你设置成多少对我们这个例子都没啥影响
    //这个值做视频的朋友可能会懂,反正我不是很懂,大概就是你预览的时候,比如你设置为10,那么你10秒内的预览图都是同一张
    format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
    //创建一个MediaCodec实例
    mediaCodec = MediaCodec.createEncoderByType("video/avc");
    //第一个参数将我们上面设置的format传进去
    //第二个参数是Surface,如果我们需要读取MediaCodec编码后的数据就要传,但我们这里不需要所以传null
    //第三个参数关于加解密的,我们不需要,传null
    //第四个参数是一个确定的标志位,也就是我们现在传的这个
    mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    //获取MediaCodec的surface,这个surface其实就是一个入口,屏幕作为输入源就会进入这个入口,然后交给MediaCodec编码
    surface = mediaCodec.createInputSurface();
    mediaCodec.start();

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

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

//第一个参数是输出的地址
//第二个参数是输出的格式,我们设置的是mp4格式
mediaMuxer = new MediaMuxer(filePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

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

virtualDisplay = mediaProjection.createVirtualDisplay(TAG + "-display",
              width, height, dpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
              surface, null, null);

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

  private void recordVirtualDisplay() {
    while (!mQuit.get()) {
      //dequeueOutputBuffer方法你可以这么理解,它会出列一个输出buffer(你可以理解为一帧画面),返回值是这一帧画面的顺序位置(类似于数组的下标)
      //第二个参数是超时时间,如果超过这个时间了还没成功出列,那么就会跳过这一帧,去出列下一帧,并返回INFO_TRY_AGAIN_LATER标志位
      int index = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
      //当格式改变的时候吗,我们需要重新设置格式
      //在本例中,只第一次开始的时候会返回这个值
      if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        resetOutputFormat();

      } else if (index >= 0) {//这里说明dequeueOutputBuffer执行正常
        //这里执行我们转换成mp4的逻辑
        encodeToVideoTrack(index);
        mediaCodec.releaseOutputBuffer(index, false);
      }
    }
  }
  //这里是将数据传给MediaMuxer,将其转换成mp4
  private void encodeToVideoTrack(int index) {
    //通过index获取到ByteBuffer(可以理解为一帧)
    ByteBuffer encodedData = mediaCodec.getOutputBuffer(index);
    //当bufferInfo返回这个标志位时,就说明已经传完数据了,我们将bufferInfo.size设为0,准备将其回收
    if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
      bufferInfo.size = 0;
    }
    if (bufferInfo.size == 0) {
      encodedData = null;
    }
    if (encodedData != null) {
      encodedData.position(bufferInfo.offset);//设置我们该从哪个位置读取数据
      encodedData.limit(bufferInfo.offset + bufferInfo.size);//设置我们该读多少数据
      //这里将数据写入
      //第一个参数是每一帧画面要放置的顺序
      //第二个是要写入的数据
      //第三个参数是bufferInfo,这个数据包含的是encodedData的offset和size
      mediaMuxer.writeSampleData(videoTrackIndex, encodedData, bufferInfo);

    }
  }

  //这个方法其实就是设置MediaMuxer的Format
  private void resetOutputFormat() {
    //将MediaCodec的Format设置给MediaMuxer
    MediaFormat newFormat = mediaCodec.getOutputFormat();
    //获取videoTrackIndex,这个值是每一帧画面要放置的顺序
    videoTrackIndex = mediaMuxer.addTrack(newFormat);
    mediaMuxer.start();
    muxerStarted = true;
  }

好了,录屏到此结束了。

我们来看下实例演示:

总结:

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

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

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

本篇博客的实例代码:

github项目地址:https://github.com/ChenTianSaber/ScreenRecorderShoter

源码下载地址:ScreenRecorderShoter_jb51.rar

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android实现拍照截图功能

    本文将向大家展示如何拍照截图. 先看看效果图: 拍照截图有点儿特殊,要知道,现在的Android智能手机的摄像头都是几百万的像素,拍出来的图片都是非常大的.因此,我们不能像对待相册截图一样使用Bitmap小图,无论大图小图都统一使用Uri进行操作. 一.首先准备好需要使用到的Uri: private static final String IMAGE_FILE_LOCATION = "file:///sdcard/temp.jpg";//temp file Uri imageUri =

  • Android实现从相册截图的功能

    在这篇文章中,我将向大家展示如何从相册截图. 先看看效果图: 上一篇文章中,我就拍照截图这一需求进行了详细的分析,试图让大家了解Android本身的限制,以及我们应当采取的实现方案.大家可以回顾一下:Android实现拍照截图功能 根据我们的分析与总结,图片的来源有拍照和相册,而可采取的操作有 使用Bitmap并返回数据 使用Uri不返回数据 前面我们了解到,使用Bitmap有可能会导致图片过大,而不能返回实际大小的图片,我将采用大图Uri,小图Bitmap的数据存储方式. 我们将要使用到URI

  • Android实现截图和分享功能的代码

    先给大家展示下效果图吧 直接上代码: xml的布局: <Button android:id="@+id/btn_jp" android:layout_marginTop="10dip" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:tex

  • Android中通过view方式获取当前Activity的屏幕截图实现方法

    此方法是通过view的方式获取当前activity的屏幕截图,并不是framebuffer的方式,所以有一定的局限性.但是这种方法相对简单,容易理解. 首先通过下面的函数获取Bitmap格式的屏幕截图: 复制代码 代码如下: public Bitmap myShot(Activity activity) { // 获取windows中最顶层的view View view = activity.getWindow().getDecorView(); view.buildDrawingCache()

  • Android模拟器中窗口截图存成文件实现思路及代码

    Android模拟器内容是用OpenGL渲染的,所以用一般的编程截图(如PrintWindow()等)会是黑屏.这是因为画的东西放在framebuffer里. 一种方法是通过adb把guest的framebuffer数据/dev/graphics/fb0倒到host,再转为图片.但这样速度比较慢. 好在Android模拟器中把guest的framebuffer传到host进行显示,所以在host端只要将framebuffer输出到文件即可. 首先定义每次framebuffer更新时的回调函数:

  • Android中如何获取视频文件的截图、缩略图

    背景 公司最近要求给我负责的APP加上视频录制和发布的功能,我简单的完成了基本的录制和视频压缩功能,后来发现发布接口需要上传视频的截图,网上搜索了一下资料,在这里整理一下. 代码实现 /** * 获取视频文件截图 * * @param path 视频文件的路径 * @return Bitmap 返回获取的Bitmap */ public static Bitmap getVideoThumb(String path) { MediaMetadataRetriever media = new Me

  • Android获取常用辅助方法(获取屏幕高度、宽度、密度、通知栏高度、截图)

    我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现.下面就分享一下Android中常用的一些辅助方法: 获取屏幕高度: /** * 获得屏幕高度 * @param context * @return * by Hankkin at:2015-10-07 21:15:59 */ public static int getScreenWidth(Context context) { WindowManager wm = (WindowManager) cont

  • 详解有关Android截图与录屏功能的学习

    简单的截屏和录屏功能. 因为MediaProjection是5.0以上才出现的,所以今天所讲述功能实现,只在5.0以上的系统有效. 截屏: 步骤如下: 1:获取MediaProjectionManager 2:通过MediaProjectionManager.createScreenCaptureIntent()获取Intent 3:通过startActivityForResult传入Intent然后在onActivityResult中通过MediaProjectionManager.getMe

  • 详解Kotlin Android开发中的环境配置

    详解Kotlin Android开发中的环境配置 在Android Studio上面进行安装插件 在Settings ->Plugins ->Browse repositores.. ->kotlin 安装完成后重启Android Studio就生效了 如图所示: 在Android Studio中做Kotlin相关配置 (1)在根目录 的build.gradle中进行配置使用,代码如下: buildscript { ext.kotlin_version = '1.1.2-4' repos

  • 详解xamarin Android 实现ListView万能适配器

    详解xamarin Android 实现ListView万能适配器 早些时候接触xamarin Android 的列表,写了很多ListView的Adapter,建一个ListView就写一个Adapter,每一个Adapter里面还有去写一个ViewHolder的类来优化,自从看了hongyang博客的listview万能适配器的文章,学习良多,所以就写篇关于xamarin android ListView通用适配器的文章. 本章主要分为以下三点: 打造通用的ViewHolder优化ListV

  • 详解Xamarin.Android 利用Fragment实现底部菜单

    本篇文章主要介绍了详解Xamarin.Android 利用Fragment实现底部菜单,分享给大家,具体如下: 效果图: 第一步:添加引用 引用 Crosslight.Xamarin.Android.Support.v7.AppCompat 这个包. 第二步:绘制Main和Fragment界面 fg_home.axml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:androi

  • android实现录屏功能

    本文实例为大家分享了android实现录屏功能的具体代码,供大家参考,具体内容如下 1.mian.activity package com.fpt.screenvideo; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.media.projection.MediaProjectionManager; import androi

  • android MediaRecorder实现录屏时带录音功能

    下面说说android的事把 最近是不是也会遇到需求中需要用到录屏录音的功能,最近也是遇到的 现在整理完记录一下 首先呢,录音录屏需要权限 先贴一个动态权限类 public class TalAllow { /** * RECORD_AUDIO 音频权限 * WRITE_EXTERNAL_STORAGE 写入权限 * CAMERA 相机权限 */ public static void requestPermissions(Context context,int allowCode) { Arra

  • Android开发实现录屏小功能

    最近开发中,要实现录屏功能,查阅相关资料,发现调用 MediaProjectionManager的api 实现录屏功能即可: import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.media.project

  • 详解基于Android的Appium+Python自动化脚本编写

    1.Appium Appium是一个开源测试自动化框架,可用于原生,混合和移动Web应用程序测试, 它使用WebDriver协议驱动iOS,Android和Windows应用程序. 通过Appium,我们可以模拟点击和屏幕的滑动,可以获取元素的id和classname,还可以根据操作生成相关的脚本代码. 下面开始Appium的配置. appPackage和APPActivity的获取 任意下载一个app 解压 但是解压出来的xml文件可能是乱码,所以我们需要反编译文件. 逆向AndroidMan

  • Android实现长截屏功能

    本文实例为大家分享了Android实现长截屏功能的具体代码,供大家参考,具体内容如下 1.MainActivity public class MainActivity extends AppCompatActivity { ScrollView scrollView; String sdRoot = Environment.getExternalStorageDirectory().getPath(); @Override protected void onCreate(Bundle saved

  • python实现录制全屏和选择区域录屏功能

    最近给客户演示程序运行结果,我就想到用Python写一个录屏程序,在网上能找到现成的源码,但是它的录屏是录制整个屏幕的.但是在屏幕桌面下方的任务栏工具栏里有些东西,不希望被录制到视频里,因此需要实现一个选择区域录屏,就像qq截图那样的.我编写的程序如下,在主函数的输入参数里有一个选项控制是全屏录制还是选择区域录制.在编写这个程序时,我有一个疑问,在初始化写视频VideoWriter函数的第4个参数,它表示视频帧的高和宽,全屏录制方式的参数是(height,width),选择区域录制的参数是(wi

随机推荐