Android 悬浮窗、屏幕推流与录屏截屏
项目需求声明
- 打开应用后直接显示桌面悬浮窗
- 点击悬浮按钮开始屏幕推流
1. 打开应用后直接显示桌面悬浮窗
针对此项目需求,需要考虑的是,我打开app后应启动Launcher Activity,然后将应用栈放到后台。需要注意的是,如果时平常的Activity,那么会闪一下屏幕,这样的效果真的让人不能接受。以下是编码思路:
1). 解决Launcher Activity闪屏问题
- 不给Launcher Activity绑定布局
- 给Launcher Activity设置Theme为透明背景:
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen"
- 启动Activity后将栈移至后台运行
moveTaskToBack(true)
2). 添加悬浮窗
- 添加权限
AndroidManifest中添加权限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
在Activity中检查悬浮窗权限
/**
* 检查权限
*/
public static boolean hasPermissionOnActivityResult(Context context) {
if (Build.VERSION.SDK_INT == 26) {
return hasPermissionForO(context);
} else {
return Build.VERSION.SDK_INT >= 23 ? Settings.canDrawOverlays(context) : hasPermissionBelowMarshmallow(context);
}
}
private static boolean hasPermissionBelowMarshmallow(Context context) {
try {
AppOpsManager manager = (AppOpsManager) context.getSystemService("appops");
Method dispatchMethod = AppOpsManager.class.getMethod("checkOp", Integer.TYPE, Integer.TYPE, String.class);
return 0 == (Integer) dispatchMethod.invoke(manager, 24, Binder.getCallingUid(), context.getApplicationContext().getPackageName());
} catch (Exception var3) {
return false;
}
}
@RequiresApi(api = 23)
private static boolean hasPermissionForO(Context context) {
try {
WindowManager mgr = (WindowManager) context.getSystemService("window");
if (mgr == null) {
return false;
} else {
View viewToAdd = new View(context);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0, Build.VERSION.SDK_INT >= 26 ? 2038 : 2003, 24, -2);
viewToAdd.setLayoutParams(params);
mgr.addView(viewToAdd, params);
mgr.removeView(viewToAdd);
return true;
}
} catch (Exception var4) {
Log.e("FHApplication", "hasPermissionForO e:" + var4.toString());
return false;
}
}
如果没有权限,则要申请权限(跳转到悬浮窗权限设置页):
if (VERSION.SDK_INT >= 23) {
this.requestAlertWindowPermission();
}
@RequiresApi(
api = 23
)
private void requestAlertWindowPermission() {
Intent intent = new Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION");
intent.setData(Uri.parse("package:" + this.getPackageName()));
this.startActivityForResult(intent, requestCode);
}
返回到Activity时,在onActivityResult方法中添加如下代码:
if (requestCode == requestCode) {
if (PermissionUtil.hasPermissionOnActivityResult(this)) {
//成功申请到权限
mPermissionListener.onSuccess();
} else {
//没有申请到权限
mPermissionListener.onFail();
}
}
- 添加悬浮窗
我使用了开源库,随便找一个就行。已经有的轮子,如非必要,不建议费时费力重新造(但请保持一颗好奇心,可以不造,但总得把实现方法搞清楚得差不多)。
2. 屏幕推流
Android 5.0开始Android开放了录屏的方法,可以直接使用。
mMediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val captureIntent = mMediaProjectionManager!!.createScreenCaptureIntent()
startActivityForResult(captureIntent, REQUEST_CODE_A)
回调
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
try {
val mediaProjection =
mMediaProjectionManager!!.getMediaProjection(resultCode, data!!)
if (mediaProjection == null) {
Toast.makeText(this, "程序发生错误:MediaProjection@1", Toast.LENGTH_SHORT).show()
return
}
mScreenRecord = ScreenRecord(this, mediaProjection)
mScreenRecord.start()
} catch (e: Exception) {
e.printStackTrace()
}
}
ScreenRecord是个线程,在里面继续处理:
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
"ScreenRecord",
width, height, dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
//编码器创建的Surface对象,API>=18时支持:codec.createInputSurface()
getVideoEncoder()!!.inputSurface,
null,
null
)
推流过程不是重点,略过。。。
3. 补充
mMediaProjection.createVirtualDisplay的调用在Android 8.0及以上必须要在前台线程中进行,否则会报异常。需声明
<service
android:name=".view.PushStreamService"
android:priority="1000"
android:exported="false"
android:enabled="true"
android:foregroundServiceType="mediaProjection">
</service>
重点时最后一行。
关于录屏和截屏
1). 录屏
Android 提供了MediaRecorder来帮助录屏(不仅仅支持录屏,还可以录制摄像头视频),可以同时录制音视频。网上的教程很多,不再赘述。
2). 截屏
Android提供了ImageReader可以使用。
ImageReader imageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 2);
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable (ImageReader reader){
Image image = reader.acquireNextImage();
//这里做一些其他处理
...
//最后必须close掉拿到的image
image.close();
}
},new Handler(Looper.getMainLooper()));
以上可以定义一个ImageReader来接收数据,但还需将ImageReader的Surface交给VirtualDisplay:
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
"ScreenRecord",
width, height, dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
imageReader.getSurface(), //这里是重点
null,
null
)
备注:
- 我在为ImageReader设置监听器时,Handler传入了主线程的Handler,这样可以正常接收到数据,但我如果传入了子线程的Handler,就无法接收到任何数据了。原因不详,如有朋友知道原因,请不吝赐教,下方留言👇,蟹蟹~
- 我在公司的Android 5.1测试机上测试,ImageReader创建时的第三个参数format传入ImageFormat.JPEG后不会接收到任何数据。然后尝试使用ImageFormat.YUV_420_888后直接报异常,异常的大致意思是i420(ImageFormat.YUV_420_888)的Format值与0x1不匹配,于是第三个参数改为0x1,可以拿到RGBA的byte数组。由此可以使用该数组还原一张完整的图像。至于为什么JPEG不回调、YUV_420_888也不回调、只有0x1才回调但是该值又不在官方给出的format的范围内,如有朋友知道原因,也请不吝赐教👇!!谢谢!!