屏幕捕捉

Android5.0之后开放了屏幕捕捉的API,因此开发者便可以直接通过代码进行截图与录屏,而无需操作系统底层了。屏幕捕捉的功能由MediaProjectionManager媒体投影管理器实现,该管理器的对象从系统服务MEDIA_PROJECTION_SERVICE中获得。注意MediaProjectionManager是Android5.0之后新增的工具,故代码中要补充判断系统版本,如果是4.*及以下版本,则不可处理屏幕捕捉操作。

具体的屏幕捕捉,还要调用媒体投影管理器对象的getMediaProjection方法,获取MediaProjection媒体投影对象。MediaProjection主要有两个方法,说明如下:
createVirtualDisplay : 创建虚拟显示层。可分别指定显示层的名称、宽度、高度、密度、标志、渲染表面等等。其中标志通常取值DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,渲染表面则按照截图和录屏两种方式分别取值。
stop : 停止投影。

屏幕捕捉的用途主要是截图和录屏,这有点像摄像头的功能,截图对应拍照,而录屏对应录像。对于拍照和录像,我们知道需要创建一个SurfaceView表面视图做为画面预览层,那么就屏幕捕捉而言,也需要创建一个虚拟显示对象做为投影预览层。这个投影预览层即前面createVirtualDisplay方法返回的VirtualDisplay对象,具体的表面对象则为createVirtualDisplay方法中的渲染表面参数,也就是一个Surface对象。如果当前为截图操作,那么调用ImageReader对象的getSurface方法获得渲染表面;如果当前为录屏操作,那么调用MediaCodec对象的createInputSurface方法获得渲染表面。

截图

给屏幕截图用到了ImageReader,它的常用方法说明如下:
newInstance : 静态函数,构造一个图像读取器,可指定图像的宽度、高度、色彩模式,以及图像数量。
getSurface : 获取图像的渲染表面。在实现截图功能时,这里的表面对象要作为createVirtualDisplay方法的输入参数。
acquireLatestImage : 获得最近的一幅图像数据。该方法返回Image对象,需转换为Bitmap格式。

下面是把Image对象转换为Bitmap格式的示例代码:

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

截图服务的主要逻辑代码如下所示:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class CaptureService extends Service implements FloatClickListener {private static final String TAG = "CaptureService";private MediaProjectionManager mMpMgr;private MediaProjection mMP;private ImageReader mImageReader;private String mImagePath, mImageName;private int mScreenWidth, mScreenHeight, mScreenDensity;private VirtualDisplay mVirtualDisplay;private FloatView mFloatView;@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic void onCreate() {super.onCreate();mImagePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/ScreenShots/";mMpMgr = MainApplication.getInstance().getMpMgr();mScreenWidth = DisplayUtil.getSreenWidth(this);mScreenHeight = DisplayUtil.getSreenHeight(this);mScreenDensity = DisplayUtil.getSreenDensityDpi(this);mImageReader = ImageReader.newInstance(mScreenWidth, mScreenHeight, PixelFormat.RGBA_8888, 2);if (mFloatView == null) {mFloatView = new FloatView(MainApplication.getInstance());mFloatView.setLayout(R.layout.float_capture);}mFloatView.setOnFloatListener(this);}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {if (mFloatView != null && mFloatView.isShow() == false) {mFloatView.show();}return super.onStartCommand(intent, flags, startId);}@Overridepublic void onFloatClick(View v) {Toast.makeText(this, "准备截图", Toast.LENGTH_SHORT).show();mHandler.postDelayed(mStartVirtual, 100); // 准备屏幕mHandler.postDelayed(mCapture, 500); // 进行截图mHandler.postDelayed(mStopVirtual, 1000); // 释放屏幕}private Handler mHandler = new Handler();private Runnable mStartVirtual = new Runnable() {@Overridepublic void run() {mFloatView.mContentView.setVisibility(View.INVISIBLE);if (mMP == null) {mMP = mMpMgr.getMediaProjection(MainApplication.getInstance().getResultCode(), MainApplication.getInstance().getResultIntent());}mVirtualDisplay = mMP.createVirtualDisplay("capture_screen", mScreenWidth, mScreenHeight,mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,mImageReader.getSurface(), null, null);}};private Runnable mCapture = new Runnable() {@Overridepublic void run() {mImageName = Utils.getNowDateTime() + ".png";Log.d(TAG, "mImageName=" + mImageName);Bitmap bitmap = FileUtil.getBitmap(mImageReader.acquireLatestImage());if (bitmap != null) {FileUtil.createFile(mImagePath, mImageName);FileUtil.saveBitmap(mImagePath+mImageName, bitmap, "PNG", 100);Toast.makeText(CaptureService.this, "截图成功:"+mImagePath+mImageName, Toast.LENGTH_SHORT).show();} else {Toast.makeText(CaptureService.this, "截图失败:未截到屏幕图片", Toast.LENGTH_SHORT).show();}}};private Runnable mStopVirtual = new Runnable() {@Overridepublic void run() {mFloatView.mContentView.setVisibility(View.VISIBLE);if (mVirtualDisplay != null) {mVirtualDisplay.release();mVirtualDisplay = null;}}};@Overridepublic void onDestroy() {if (mFloatView != null && mFloatView.isShow() == true) {mFloatView.close();}if (mMP != null) {mMP.stop();}super.onDestroy();}}

录屏

把用户在屏幕上的操作行为录制成视频,我们称之为录屏。因为视频有多种格式,不同格式的编码过程也不相同,所以录屏的过程比起截图要复杂得多,主要功能点简述如下:
1、需要控制何时开始录屏,何时结束录屏;
2、设置视频的编码格式,及其对应的编码过程;
3、指定视频的常见播放参数,如尺寸、位率、帧率、色彩等等;
具体到编码实现上,录屏使用了MediaCodec媒体编码器和MediaMuxer媒体转换器两个工具,通过这两个工具的相互配合,方能完成屏幕录制功能。

下面是媒体编码器MediaCodec的主要方法说明:
createEncoderByType : 静态函数,根据编码格式构造一个媒体编码器。编码格式通常取值MediaFormat.MIMETYPE_VIDEO_AVC。
configure : 设置媒体编码的参数,包括视频格式、视频宽高、视频位率、视频帧率等等。
createInputSurface : 创建一个用于输入的表面对象。在实现录屏功能时,这里的表面对象要作为createVirtualDisplay方法的输入参数。
start : 开始编码。
dequeueOutputBuffer : 给输出缓冲区排队。返回该输出缓冲区的索引位置。
getOutputFormat : 获取输出格式。
getOutputBuffer : 根据索引位置获取输出缓冲区的数据。
releaseOutputBuffer : 释放指定索引位置的输出缓冲区。
stop : 停止编码。
release : 释放媒体编码资源。

下面是媒体转换器MediaMuxer的主要方法说明:
构造函数 : 根据文件路径与文件格式构造一个媒体转换器。文件格式通常取值MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4。
addTrack : 把指定格式添加到转换轨道上。返回轨道的索引位置。
start : 开始工作。
writeSampleData : 把编码转换后的数据写入索引位置的轨道。该方法在MediaCodec的getOutputBuffer方法之后调用。
stop : 停止工作。
release : 释放媒体转换资源。

由于截图和录屏可用于捕捉其它App的画面,为了让录屏App在其它界面上也能响应控制操作,因此要把录屏App的控制条做成悬浮窗的样式,通过悬浮窗按钮完成截图或者录屏功能。有关悬浮窗的说明参见《 Android开发笔记(一百一十八)自定义悬浮窗》。

录屏服务的主要逻辑代码如下所示:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class RecordService extends Service implements FloatClickListener {private static final String TAG = "RecordService";private String mVideoPath, mVideoName;private MediaProjectionManager mMpMgr;private MediaProjection mMP;private VirtualDisplay mVirtualDisplay;private int mScreenWidth, mScreenHeight, mScreenDensity;private MediaCodec mMediaCodec;private MediaMuxer mMediaMuxer;private boolean isRecording = false, isMuxerStarted = false;private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();private int mVideoTrackIndex = -1;private FloatView mFloatView;private ImageView iv_record;@Overridepublic IBinder onBind(Intent intent) {return null;}@Overridepublic void onCreate() {super.onCreate();mVideoPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/ScreenRecords/";mMpMgr = MainApplication.getInstance().getMpMgr();mScreenWidth = DisplayUtil.getSreenWidth(this);mScreenHeight = DisplayUtil.getSreenHeight(this);mScreenDensity = DisplayUtil.getSreenDensityDpi(this);if (mFloatView == null) {mFloatView = new FloatView(MainApplication.getInstance());mFloatView.setLayout(R.layout.float_record);}mFloatView.setOnFloatListener(this);iv_record = (ImageView) mFloatView.mContentView.findViewById(R.id.iv_record);}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {if (mFloatView != null && mFloatView.isShow() == false) {mFloatView.show();}return super.onStartCommand(intent, flags, startId);}@Overridepublic void onFloatClick(View v) {isRecording = !isRecording;if (isRecording) {iv_record.setImageResource(R.drawable.ic_record_pause);Toast.makeText(RecordService.this, "开始录屏", Toast.LENGTH_SHORT).show();recordStart();} else {iv_record.setImageResource(R.drawable.ic_record_begin);Toast.makeText(RecordService.this, "结束录屏:"+mVideoPath+mVideoName, Toast.LENGTH_SHORT).show();}}private String prepare() {MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mScreenWidth, mScreenHeight); //视频格式与宽高format.setInteger(MediaFormat.KEY_BIT_RATE, 300*1024*8); //每秒多少位,这里设置每秒300Kformat.setInteger(MediaFormat.KEY_FRAME_RATE, 20); //每秒多少帧,每秒20帧则每帧大小15Kformat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); //设置颜色格式format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); //设置关键帧的间隔try {mMediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);mMediaCodec.start(); //开始视频编码return null;} catch (Exception e) {e.printStackTrace();return e.getMessage();}}private void recordStart() {String result = prepare();if (result != null) {Toast.makeText(this, "准备录屏发生异常:"+result, Toast.LENGTH_SHORT).show();return;}if (mMP == null) {mMP = mMpMgr.getMediaProjection(MainApplication.getInstance().getResultCode(), MainApplication.getInstance().getResultIntent());}mVirtualDisplay = mMP.createVirtualDisplay("ScreenRecords", mScreenWidth, mScreenHeight, mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaCodec.createInputSurface(), null, null);new RecordThread().start();}private class RecordThread extends Thread {@Overridepublic void run() {try {Log.d(TAG, "RecordThread Start");FileUtil.createDir(mVideoPath);mVideoName = Utils.getNowDateTime() + ".mp4"; //文件格式为MPEG-4mMediaMuxer = new MediaMuxer(mVideoPath+mVideoName, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);while (isRecording) {int index = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 10000); //返回缓冲区的索引Log.d(TAG, "缓冲区的索引为" + index);if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { //输出格式发生变化if (isMuxerStarted) {throw new IllegalStateException("输出格式已经发生变化");}MediaFormat newFormat = mMediaCodec.getOutputFormat();mVideoTrackIndex = mMediaMuxer.addTrack(newFormat);mMediaMuxer.start();isMuxerStarted = true;Log.d(TAG, "新的输出格式是:"+newFormat.toString()+",媒体转换器的轨道索引是"+mVideoTrackIndex);} else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) { //请求超时Thread.sleep(50);} else if (index >= 0) { //正常输出if (!isMuxerStarted) {throw new IllegalStateException("媒体转换器尚未添加格式轨道");}encodeToVideo(index);mMediaCodec.releaseOutputBuffer(index, false);}}} catch (Exception e) {e.printStackTrace();} finally {release();}}}private void encodeToVideo(int index) {ByteBuffer encoded = mMediaCodec.getOutputBuffer(index);if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { //如果不是媒体数据mBufferInfo.size = 0;}if (mBufferInfo.size == 0) { //缓冲区不存在有效数据encoded = null;} else {Log.d(TAG, "缓冲区大小=" + mBufferInfo.size+ ", 持续时间=" + mBufferInfo.presentationTimeUs+ ", 偏移=" + mBufferInfo.offset);}if (encoded != null) {encoded.position(mBufferInfo.offset);encoded.limit(mBufferInfo.offset + mBufferInfo.size);mMediaMuxer.writeSampleData(mVideoTrackIndex, encoded, mBufferInfo); //写入视频文件}}private void release() {isRecording = false;isMuxerStarted = false;if (mMediaCodec != null) {mMediaCodec.stop();mMediaCodec.release();mMediaCodec = null;}if (mVirtualDisplay != null) {mVirtualDisplay.release();mVirtualDisplay = null;}if (mMediaMuxer != null) {mMediaMuxer.stop();mMediaMuxer.release();mMediaMuxer = null;}}@Overridepublic void onDestroy() {release();if (mFloatView != null && mFloatView.isShow() == true) {mFloatView.close();}if (mMP != null) {mMP.stop();}super.onDestroy();}
}

点此查看Android开发笔记的完整目录

Android开发笔记(一百三十)截图和录屏相关推荐

  1. Android开发笔记(三十六)展示类控件

    View/ViewGroup View是单个视图,所有的控件类都是从它派生出来:而ViewGroup是个视图组织,所有的布局视图类都是从它派生出来.由于View和ViewGroup是基类,因此很少会直 ...

  2. Android开发笔记(三十八)列表类视图

    AdapterView AdapterView顾名思义是适配器视图,Spinner.ListView和GridView都间接继承自AdapterView,这三个视图都存在多个元素并排展示的情况,所以需 ...

  3. Android开发笔记(三十五)页面布局视图

    布局视图的类别 布局视图有五类,分别是线性布局LinearLayout.相对布局RelativeLayout.框架布局FrameLayout.绝对布局AbsoluteLayout.表格布局TableL ...

  4. Android开发笔记(三十九)Activity的生命周期

    与生命周期有关的方法 下面是Activity类与生命周期有关的方法: onCreate : 创建页面 onStart : 开始页面 onStop : 停止页面 onResume : 恢复页面 onPa ...

  5. Android开发笔记(三十四)Excel文件的读写

    Android中操作Excel文件的场合较少见,主要是一些专业领域导入导出报表时使用,所以处理Excel读写的开源代码也很稀缺.目前读写Excel主要采用开源库jxl,这个是韩国人写的excel操作工 ...

  6. Android开发笔记(三十二)文件基础操作

    File类 File类是java中的文件操作工具类,它的常用方法如下: File构造函数 : 根据文件路径构造File对象 delete : 删除文件 exists : 判断文件是否存在 getNam ...

  7. Android开发笔记(三十)SQLite数据库基础操作

    SQLite语法 SQLite是一个小巧的嵌入式数据库,使用方便.开发简单,手机上最早由ios运用,后来android兴起同样也采用了sqlite.sqlite的多数sql语法与oracle是一样的, ...

  8. Android开发笔记(八十九)单例模式

    基本概念 单例模式是一种常用的软件设计模式,它确保一个类只有一个实例,从而方便对实例个数的控制并节约系统资源. 单例模式有三个特点: 1.某个类只能有一个实例: 2.它要自行创建这个实例: 3.它只有 ...

  9. Android开发笔记(八十六)几个特殊的类

    接口interface interface是一些功能的集合,但它只定义了对象必须实现的成员,而不包含成员的实现代码,成员的具体代码由实现接口的类提供.Android对接口的使用场景主要有三类:事件监听 ...

  10. Android开发笔记(七十五)内存泄漏的处理

    内存泄漏的原因 一直以来以为只有C/C++才存在内存泄漏的问题,没想到拥有内存回收机制的Java也可能出现内存泄漏.C/C++存在指针的概念,程序中需要使用指针变量时,就从内存中开辟一块区域,并把该区 ...

最新文章

  1. matlab画三维图
  2. 【暴力】UVALive - 4882 - Parenthesis
  3. OS - MMAP初探
  4. MySQL高级 - 锁 - InnoDB行锁 - 类型
  5. 各种各种的公共工具类
  6. 棒棒糖 宏_棒棒糖图表
  7. 《局域网聊天——Android》
  8. 取值方法_数据维度爆炸怎么办?详解 5 大常用的特征选择方法
  9. 很值得学习的java 画图板源码
  10. 计算机快捷方式app卸载,一打开电脑就自动出现的快捷方式软件删不掉怎么办
  11. 知了课堂 python_知了课堂Python Flask系列(1)-基础篇 flask视频教程下载
  12. 医院HIS系统厂家统计
  13. 【Excel】Excel条件格式设置背景色
  14. android仿ios消息框,Android仿IOS提示框
  15. 金融工程学(五):互换概述
  16. Oracle篇--04 Oracle SQL高级查询、分页查询
  17. python和vb基础哪个简单_python和VB哪个更容易学习入门呢?
  18. HDU 3698-Let the light guide us(线段树+DP)愿圣光忽悠你
  19. ap协议java_总结无线AP与AC之间的各种问题
  20. [C#问题--WebBrowser继续追踪]WebBrowser在Form程序中使用的感想2

热门文章

  1. Computer science 概念汇总
  2. Algorithm:多维数组和矩阵
  3. 蓝桥杯 算法训练 Sticks
  4. 吴恩达机器学习【第三天】线性代数基础知识
  5. Jenkins学习三:介绍一些Jenkins的常用功能
  6. 详解vue动画的封装
  7. htmlcss面试笔记
  8. 通用印刷体文字识别_五个超级实用的OCR文字识别小程序,完全免费、值得收藏!...
  9. 品质主管每日工作需要做哪些_游戏配音需要做哪些工作?
  10. 若依集成knife4j实现swagger文档增强