首页 > 代码库 > Android录屏应用开发研究
Android录屏应用开发研究
1截屏接口
在Android5.0之前如果希望截图屏幕,是需要获取系统root权限的。但在Android5.0之后Android开放了新的接口android.media.projection,开发者使用该接口,第三方应用程序无需再获取系统root权限也可以直接进行屏幕截图操作了。查询其官方api可知,该接口主要用来“屏幕截图”操作和“音频录制”操作,这里只讨论用于屏幕截图的功能。由于使用了媒体的映射技术手段,故截取的屏幕并不是真正的设备屏幕,而是截取的通过映射出来的“虚拟屏幕”。不过,因为截图我们希望的得到的肯定是一张图而已,而“映射”出来的图片与系统屏幕完全一致,所以,对于普通截屏操作,该方法完全可行。
下面是该接口的使用方法:
(1)首先用参数MEDIA_PROJECTION_SERVICE调用Context.getSystemService(),得到MediaProjectionManager类别实例。
mMediaProjectionManager=(MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
(2)其次,调用 createScreenCaptureIntent()得到一个Intent;再次,使用startActivityForResult()启动屏幕捕捉,在onActivityResult()中获取resultCode和resultData,以方便下面getMediaProjection()使用。
startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION);
(3)将结果返回到getMediaProjection()上,获取捕捉数据。
mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
(4)使用mMediaRecorder实例通过getSurface()方法获取屏幕表层,使用上一步中的MediaProjection临时实例通过createVirtualDisplay()方法进行虚拟屏幕的显示。
return mMediaProjection.createVirtualDisplay("MainActivity", mScreenWidth, mScreenHeight, mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder.getSurface(), null /* Callbacks */, null /* Handler */);
2悬浮窗口
首先是一个小的悬浮窗显示在屏幕右边界面,点击一下小悬浮窗,就会弹出一个大的悬浮窗,有相关按钮进行操作。
先谈一下基本的实现原理,实现这种桌面悬浮窗的效果同Widget比较类似,但是它比Widget要灵活的多。主要是通过WindowManager这个类来实现的,调用这个类的addView方法用于添加一个悬浮窗,updateViewLayout方法用于更新悬浮窗的参数,removeView用于移除悬浮窗。
WindowManager.LayoutParams这个类用于提供悬浮窗所需的参数,其中有几个经常会用到的变量:
1)type值用于确定悬浮窗的类型,一般设为TYPE_PHONE(2002),表示在所有应用程序之上,但在状态栏之下。
2)flags值用于确定悬浮窗的行为,比如说不可聚焦,非模态对话框等等,属性非常多。
3)gravity值用于确定悬浮窗的对齐方式,一般会设为左上角对齐,这样当拖动悬浮窗的时候方便计算坐标。
4)x值用于确定悬浮窗的位置,如果要横向移动悬浮窗,就需要改变这个值。
5)y值用于确定悬浮窗的位置,如果要纵向移动悬浮窗,就需要改变这个值。
6)width值用于指定悬浮窗的宽度。
7)height值用于指定悬浮窗的高度。
创建悬浮窗这种窗体需要向用户申请权限才可以的,因此还需要在AndroidManifest.xml中加入<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />,这在api23之前可以直接在Manifest.xml中加入权限,但在android6.0之后,需要用户在系统应用权限管理中开启权限。
创建浮窗的方法:
(1)首先新建一个名为float_window_small.xml的布局文件,用于作为小悬浮窗的布局,再新建一个名为float_window_big.xml的布局文件,用于作为大悬浮窗的布局。
(2)新建一个名为FloatWindowService的类,这个类继承自Service。
(3)如上图所示在FloatWindowService的onStartCommand方法中开启了一个定时器Timer,每隔500毫秒(0.5秒)就执行一次RefreshTask()。RefreshTask继承TimerTask,在RefreshTask当中,要进行判断,如果手机当前是在桌面的话,就应该显示悬浮窗,如果手机返回了开启浮窗的应用程序,就应该移除悬浮窗。而当FloatWindowService被销毁的时候,应该将定时器停止,否则它还会一直运行。
创建和移除悬浮窗,都是由MyWindowManager这个类来管理的,比起直接把这些代码写在Activity或Service当中,使用一个专门的工具类来管理要好的多。不过要想创建悬浮窗,还是先要把悬浮窗的View写出来。新建一个名叫FloatWindowSmallView的类,继承自LinearLayout。新建一个名叫FloatWindowBigView的类,也继承自LinearLayout。
(4)在FloatWindowSmallView中,通过onTouchEvent(MotionEvent event)判断手指在屏幕的操作:
1)ACTION_DOWN:记录手指按下时必要的数据,纵坐标的值都需要减去状态栏高度。
2)ACTION_MOVE:手指移动的时候使用updateViewPosition()方法更新小悬浮窗的位置。
3)ACTION_UP:如果手指离开屏幕时,xDownInScreen和xInScreen相等,且yDownInScreen和yInScreen相等,则视为触发了单击事件,通过openBigWindow()开启大浮窗。
(5)在FloatWindowBigView中,设置2个按钮startrecord和stoprecord,对startrecord设置监听:
1.startrecord:移除大悬浮窗,创建小悬浮窗,开启ScreenrecordService。
2.stoprecord:移除大悬浮窗,创建小悬浮窗,关闭ScreenrecordService。
3.利用onTouchEvent(MotionEvent event)的case MotionEvent.ACTION_DOWN,移除大悬浮窗,创建小悬浮窗。
3视频录制
在ScreenrecordService中结合截屏完成视频的录制,ScreenrecordService继承Service。为了增加对录制音视频的支持,Android系统提供了一个MediaRecorder的类。该类的使用也非常简单,下面让我们来了解一下这个类。
与MediaPlayer类非常相似MediaRecorder也有它自己的状态图。下面是关于MediaRecorder的各个状态的介绍:
1)Initial:初始状态,当使用new()方法创建一个MediaRecorder对象或者调用了reset()方法时,该MediaRecorder对象处于Initial状态。在设定视频源或者音频源之后将转换为Initialized状态。另外,在除Released状态外的其它状态通过调用reset()方法都可以使MediaRecorder进入该状态。
2)Initialized:已初始化状态,可以通过在Initial状态调用setAudioSource()或setVideoSource()方法进入该状态。在这个状态可以通过setOutputFormat()方法设置输出格式,此时MediaRecorder转换为DataSourceConfigured状态。另外,通过reset()方法进入Initial状态。
3)DataSourceConfigured:数据源配置状态,这期间可以设定编码方式、输出文件、屏幕旋转、预览显示等等。可以在Initialized状态通过setOutputFormat()方法进入该状态。另外,可以通过reset()方法回到Initial状态,或者通过prepare()方法到达Prepared状态。
4)Prepared:就绪状态,在DataSourceConfigured状态通过prepare()方法进入该状态。在这个状态可以通过start()进入录制状态。另外,可以通过reset()方法回到Initialized状态。
5)Recording:录制状态,可以在Prepared状态通过调用start()方法进入该状态。另外,它可以通过stop()方法或reset()方法回到Initial状态。
6)Released:释放状态(官方文档给出的词叫做Idle state 空闲状态),可以通过在Initial状态调用release()方法来进入这个状态,这时将会释放所有和MediaRecorder对象绑定的资源。
7)Error:错误状态,当错误发生的时候进入这个状态,它可以通过reset()方法进入Initial状态。
下面介绍本应用关于视频录取的步骤:
1)首先获取像素,通过getDisplayMetrics()获取metrics,得到手机屏幕的分辨率。
DisplayMetrics metrics = new DisplayMetrics(); metrics = getResources().getDisplayMetrics();// service中获取displaymetrics mScreenDensity = metrics.densityDpi; mScreenWidth = metrics.widthPixels; mScreenHeight = metrics.heightPixels;
2)由于我们需要将录制的视频存储到手机内存中,因此将存储路径设置到手机的公用存储视频的文件夹下,获取路径如下所示:
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)。
3)获得MediaRecorder的实例,初始化MediaRecorder:
设置用于录制的视频来源setVideoSource(MediaRecorder.VideoSource.SURFACE);
设置声音来源:setAudioSource(MediaRecorder.AudioSource.MIC);
设置所录制的音视频文件的格式:setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
设置所录制视频的编码格式:setVideoEncoder(MediaRecorder.VideoEncoder.H264);
设置所录制的声音的编码格式:setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
设置所录制的声音的编码位率:setVideoEncodingBitRate(512 * 1000);
设置录制视频的捕获帧速率:setVideoFrameRate(40);
设置要拍摄的宽度和视频的高度:setVideoSize(mScreenWidth, mScreenHeight);
设置录制视频的存储路径:setOutputFile(mVecordFile.getAbsolutePath());
4)调用MediaRecorder的prepare()方法,完成录制视频的准备工作。
5)在ScreenrecordService中的onStartCommand(Intent intent, int flags, int startId),调用startrecord()方法。
(1)从ShotApplication共享类中获取resultCode、data和MediaProjectionManager实例。
(2)通过下面mMediaRecorder.getSurface()的串联,我们把mMediaProjection的输出内容放到了Surface里面,而Surface正是MediaRecorder的输入源,这样就完成了对mMediaProjection输出内容的编码,也就是屏幕采集数据的编码mVirtualDisplay。
return mMediaProjection.createVirtualDisplay("MainActivity", mScreenWidth, mScreenHeight, mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder.getSurface(), null /* Callbacks */, null /* Handler */);
(3)调用MediaRecorder的start()方法,开始进行录制。
6)在ScreenrecordService的onDestroy()方法中:
(1)调用MediaRecorder的stop()方法和reset()方法,完成录制
mMediaRecorder.stop();
mMediaRecorder.reset();
(2)对mVirtualDisplay进行释放。
if (mVirtualDisplay == null) { return; } mVirtualDisplay.release();
(3)停止截屏,并置空。
if (mMediaProjection != null) { mMediaProjection.stop(); mMediaProjection = null; }
(4)发送广播,通知系统媒体库更新。
sendBroadcast(new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://"+sampleDir.getAbsolutePath().toString())));
4权限获取
在android 6.0之前,应用的权限在AndroidManifest.xml静态申请就可以,但出了android 6.0之后,新的权限获取方式除了要求像之前版本一样在AndroidManifest文件中静态申请之外,应用还需根据需要请求权限,方式采用向用户显示一个请求权限的对话框。这些被动态申请的权限可以在系统设置中被手动关闭。另外,对于类别为NORMAL的权限,仍然只需要在AndroidManifest文件中静态申请,系统安装时会直接获取。
在系统授权弹窗环节,提醒框会有个不再提示的复选框,如果用户点击不太提示,并拒绝授权,那么再下次授权的时候,系统授权弹窗的提示框就不会在提示,所以我们很有必要需要自定义权限弹窗提示框,那么流程图就变成如下了。
录屏应用需要动态获取的权限如下:
(1)<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
(2)<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
(3)<uses-permission android:name="android.permission.RECORD_AUDIO"/>
以上分别是开启悬浮窗口、写入SDCARD存储和开启麦克风的权限。上述三种权限分为2种类型:
<1>系统权限:
其中写入SDCARD存储和开启麦克风的权限,为系统权限的dangerous类型,需要动态申请。处理权限申请:
1)使用方法checkSelfPermission()判定是否有权限。
2)如果没有权限,弹出dialog给用户选择:requestPermission(),第二个参数code与onRequestPermissionResult()方法中的code对应。
3)判断用户是否确认了权限onRequestPermissionResult ()。
4)在弹出权限选择的对话框前给用户show一个dialog,用于引导用户进行选择。
由于本应用需申请2个权限,因此不能按照上述申请单个权限的方法进行申请。一次性处理多个权限申请:
1)在requestPermissions()方法中,定义一个列表permissionsNeeded,通过方法addPermission()将需要申请的权限加入到列表中。
2)通过for循环弹出申请对话框,供用户选择申请权限是否应许。
3)如果所有权限被授权,依然回调onRequestPermissionsResult,检验申请的权限是否被授权。
4)在MainActivity的onCreate中使用if (Build.VERSION.SDK_INT >= 23) 判断程序进行权限授权,即运行requestPermissions()方法。
<2>特殊权限授权
开启悬浮窗口权限是特殊权限授权,除此还有一个特殊权限授权(android.permission.WRITE_SETTINGS),对于这两种特殊权限,处理如下:
如果是针对 “android.permission.SYSTEM_ALERT_WINDOW”和
“android.permission.WRITE_SETTINGS”这两个权限,如果是运行在6.0的版本上是需要走新的权限模型,如果是运行在老的版本上,则需要进行一个判断,此时碰到一个问题是,在谷歌官方推荐中,在判断app运行的系统是否在Android M上时,它的判断是如下:Build.VERSION.CODENAME.equals("MNC");实际发现这句却无效的,改用Build.VERSION.SDK_INT >= 23的,需要另外特殊处理。
针对SYSTEM_ALERT_WINDOW权限,需要向系统发送一个ACTION_MANAGE_OVERLAY_PERMISSION。这样一个动作,同时可以用Settings.canDrawOverlays() 方法进行判断之前是否已经授权过了。
//检查开启浮窗的特殊权限是否被授权 if (!Settings.canDrawOverlays(getApplicationContext())) { requestAlertWindowPermission(); }
针对WRITE_SETTINGS权限,需要向系统发送一个ACTION_MANAGE_WRITE_SETTINGS 这样一个动作,同时可以用Settings.System.canWrite().方法进行判断之前是否已经授权过了。
5录屏应用的流程研究
录屏应用的实现思路大致是这样:
1、抛弃应用内界面按钮开启关闭录屏,采用浮动小图标和点击出现的大图标实现录屏按键。浮动图标的实现通过FloatWindowService,它继承自Service类,这样可以方便地创建只有浮动图标的布局,在Activity等地方利用startService(Intent intent)方法开启服务。
2、我们希望用户可以在界面随时拖动小图标,小图标位置可变,在点击小图标时,出现大图标并且,点击按钮、以及图标外围,大图标移除,重建小图标。
3、图像保存为MP4格式,路径为手机sdcard的Movies文件目录,自建的文件夹RecordVideo。
4、浮动小球的优先级为一般应用的最顶层,即除了状态栏下拉列表外,小球总是可见的,这要得益于Service类的性质了。虽然在看电视等环境下会比较不适合,但该设计能让用户随时、方便地录取到想要的屏幕图像。
一、截屏请求结果数据共享类ShotApplication
在上面已经提到,屏幕获取需要用户同意,初次运行时会有请求对话框,同意之后才能继续,否则程序会终止。既然需要用户选择后的信息,那在发出截屏请求时就不能用简单的startActivity(Intent intent)方法,而是要用startActivityForResult(Intent intent, intresquestCode)方法。但是Service类中startActivityForResult(Intent intent, int resquestCode)方法不可用,确切的说是不存在可供子类重载的onActivityResult(int resquestCode, int resultCode, Intent data) 方法。但现实是ScreenrecordService类在实现录屏过程中又要用到后面两个返回值(resultCode与data)来构建MediaProjection类的对象。
如果直接在Activity中进行录屏不就可以了吗,这样做没有问题。但是问题在于我们需要在任何想截取屏幕的时候就能快速、方便地进行,即需要借助利用FloatWindowService实现并浮动在一般性应用窗口之上的小球。而在Activity中实现的话就达不到这种效果了,往往能获取的只能是应用本身界面,或者是将其隐藏后的下一层界面,总之做不到想要即可得的效果。
所以,首要问题是让类ScreenrecordService的对象能得到这两个数据。另外得注意,Bundle可以完成一般数据的加载并赋给Intent类对象,然后一起发送给目标类,但参数data本身就是Intent类型的。我们在这里采取数据共享的思路,利用继承自Application类的子类ShotApplication,然后定义需要共享的成员变量(有些是其他类的对象)。
其中MediaProjectionManager类对象在发送截屏请求和构建MediaProjection类对象时均会用到,至于成员值的设置及获取很直观,就不解释了。那么数据的传递就明朗了:先从主程序类MainActivity中存入共享类ShotApplication,然后服务类ScreenrecordService从共享类ShotApplication中提取出来。
二、主程序类MainActivity所做5件事
1)首先判断android平台,如果是api>=23,先进行权限的动态申请。
2)向用户提出截屏请求。这正是类MainActivity做的第1件事。当然,在这之前需要获取类MediaProjectionManager实例。
由于onCreate()方法是应用开启后自动调用的,在点击start按钮后startIntent随即被调用,所以第一次运行时,这一行截屏请求代码也会自动执行。如果是应用安装后第一次开启,那么就会弹出截屏权限允许对话框,需要用户授权。
3)类MainActivity做的第2件事就是将用户操作所返回的值和初始获取的类MediaProjectionManager实例写入数据共享类ShotApplication中。
4)类MainActivity做的第3件事就是肯定是开启浮窗服务。
5)类MainActivity做的第4件事是将自身销毁(finish()),之后的控制权就交给服务类的浮动小窗体。
三、服务类FloatWindowService完成大小悬浮窗的创建、移除和控制
从onStartCommand(Intent intent, int flags, int startId)方法开始,开启定时器,每隔0.5秒调用RefreshTask()。
1)如果当前界面是桌面,且没有悬浮窗显示,则创建悬浮窗,调用createSmallWindow()。
2)当前界面是返回应用的界面,且有悬浮窗显示,则移除悬浮窗,调用removeSmallWindow()和removeBigWindow()。
3)在类FloatWindowSmallView中监听图标的点击事件,调用createBigWindow()。
4)在类FloatWindowBigView中监听按钮的点击事件:
1.点击开始录制的时候,移除大悬浮窗,创建小悬浮窗,并开始ScreenrecordService。
2. 点击结束录制的时候,移除大悬浮窗,创建小浮窗,并停止ScreenrecordService。
四、服务类ScreenrecordService完成录屏工作
(1)在onCreate()中调用了createRecordDir()、initRecorder()和prepareRecorder()方法。
1)createRecordDir()用来创建录制视频的保存路径;
2)initRecorder()负责MediaRecorder的初始化;
3)prepareRecorder()负责调用MediaRecorder实例的prepare()方法。
(2)在onStartCommand中调用startrecord()负责开启截屏,创建一个Surface,并录制视频。
(3)在该ScreenrecordService的onDestroy()中停止视频的录制,并停止截屏。
(4)发送广播,通知系统媒体库更新。
6总结
(1)截屏功能是在Android5.0才开始加入的(或者说开放出来的)新接口,所以,该方法只能用于5.0以上的Android版本(api21以上)。
(2)浮窗通过WindowManager这个类来实现的,调用这个类的addView方法可以添加一个悬浮窗。
(3)应用MediaRecorder时应注意,与MediaPlayer相似使用MediaRecorder录音录像时需要严格遵守状态图说明中的函数调用先后顺序,在不同的状态调用不同的函数,否则会出现异常。
(4)Android 6.0之后,新的权限获取方式除了要求像之前版本一样在AndroidManifest文件中静态申请之外,应用还需根据需要请求的权限,方式采用向用户显示一个请求权限的对话框,才能获取相应的权限。
(5)在Service类中startActivityForResult(Intent intent, int resquestCode)方法不可用,确切的说是不存在可供子类重载的onActivityResult(int resquestCode, int resultCode, Intent data) 方法,我们可以采用共享类的思路获取resultCode和data。
Android录屏应用开发研究