Android悬浮窗视频

Myth丶恋晨 2023-07-11 05:34 91阅读 0赞

类似微信视频通话需求。
思路:
1.申请悬浮窗权限
2.windowManager实现悬浮窗;
3.moveToback退出全屏,显示悬浮窗;

当用户正在NewsActivity看文字,视频通话来了,接听(VideoActivity),然后缩至悬浮窗,此时应回到NewsActivity,悬浮窗出现时该如何回到接听(VideoActivity)前的页面?finish掉VideoActivity吗?finish后自然回退到栈内上一个Activity,页面逻辑上符合需求,但是VideoActivity中写了很多通话逻辑,销毁不太好,怎么办?(有人说,为何会在页面写逻辑,明显是没抽象好嘛,这都是后话,不解决当前版本问题)

于是想到干脆起两个任务栈,一个单独放VideoActivity,另一个放剩余的Activity,然后点击要悬浮时,直接后置VideoActivity所在任务栈即可。
实现两个任务栈也很简单,只要给VideoActivity设置android:launchMode=”singleInstance”即可。
想要退后该任务栈直接调用 moveTaskToBack(true);

然而singleInstance的坑不是一般的多。

一 . 首先 singleInstance会导致onRequestPermissionsResult( ),onActivityResult( )不回调。可是视频必须要申请RECORD_AUDIO,CAMERA,WRITE_EXTERNAL_STORAGE权限,以及悬浮窗ACTION_MANAGE_OVERLAY_PERMISSION。
无奈写个视频的前置页面标准启动模式,专门用来申请相机,录音,读写等权限,全部授权后 再真正进入singleInstance页面。
问题一算是迎刃而解。(躲过去了)

二. 长按home或者菜单键 查看Android最近任务列表时,一个APP竟然两个最近任务,解决方案:需要添加一个属性 保证 不在最近任务列表中显示当前activity所在的应用

  1. android:launchMode= "singleInstance" //开启新的应用任务栈
  2. android:excludeFromRecents= "true" //不在最近任务列表中显示当前activity所在的应用

三 选择TYPE_TOAST,如果期间有toast弹出,在android7.1.1会崩溃,加上版本判断吧。

  1. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  2. wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
  3. } else {
  4. wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
  5. }

下面是悬浮窗的实现

  1. /** * 视频面试悬浮窗 */
  2. public class FloatVideoWindowService extends Service {
  3. private WindowManager mWindowManager;
  4. private WindowManager.LayoutParams wmParams;
  5. private View mFloatingLayout;
  6. private RelativeLayout mPreviewLayout;
  7. private TutorialsManager mInstance = null;
  8. private SurfaceView remoteView = null;
  9. @Override
  10. public IBinder onBind(Intent intent) {
  11. return new MyBinder();
  12. }
  13. public class MyBinder extends Binder {
  14. // public FloatVideoWindowService getService() {
  15. // return FloatVideoWindowService.this;
  16. // }
  17. }
  18. @Override
  19. public void onCreate() {
  20. super.onCreate();
  21. initWindow();//设置悬浮窗基本参数(位置、宽高等)
  22. initFloating();//悬浮框点击事件的处理
  23. mInstance = TutorialsManager.getInstance(this);
  24. }
  25. @Override
  26. public int onStartCommand(Intent intent, int flags, int startId) {
  27. int uid = intent.getIntExtra(VideoChatViewActivity.KEY_C_USER_ID, -1);
  28. if (uid != -1) {
  29. remoteView = mInstance.getRemoteVideo(uid);
  30. ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
  31. remoteView.setLayoutParams(lp);
  32. mPreviewLayout.addView(remoteView);
  33. } else {
  34. stopSelf();
  35. }
  36. return super.onStartCommand(intent, flags, startId);
  37. }
  38. @Override
  39. public void onDestroy() {
  40. if (null != mPreviewLayout) {
  41. mPreviewLayout.removeAllViews();
  42. }
  43. if (mFloatingLayout != null) {
  44. // 移除悬浮窗口
  45. mWindowManager.removeView(mFloatingLayout);
  46. }
  47. }
  48. /** * 设置悬浮框基本参数(位置、宽高等) */
  49. private void initWindow() {
  50. mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
  51. wmParams = getParams();
  52. // 悬浮窗默认显示以左上角为起始坐标
  53. wmParams.gravity = Gravity.RIGHT | Gravity.TOP;
  54. //悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0
  55. wmParams.x = DensityUtil.dip2px(10);
  56. wmParams.y = 110;
  57. // 获取浮动窗口视图所在布局
  58. mFloatingLayout = LayoutInflater.from(getApplicationContext()).inflate(R.layout.view_videochat_services_float_layout, null);
  59. // 添加悬浮窗的视图
  60. mWindowManager.addView(mFloatingLayout, wmParams);
  61. }
  62. private WindowManager.LayoutParams getParams() {
  63. wmParams = new WindowManager.LayoutParams();
  64. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  65. wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
  66. } else {
  67. wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
  68. }
  69. //设置可以显示在状态栏上
  70. wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams
  71. .FLAG_NOT_TOUCH_MODAL |
  72. WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR |
  73. WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
  74. int fullCropWidth = DeviceUtils.getScreenWidth();
  75. int cropHeight = fullCropWidth * 16 / 9;
  76. //设置悬浮窗口长宽数据
  77. wmParams.width = (int) (fullCropWidth * 0.26);
  78. wmParams.height = (int) (cropHeight * 0.26);
  79. return wmParams;
  80. }
  81. private void initFloating() {
  82. mPreviewLayout = mFloatingLayout.findViewById(R.id.small_size_preview);
  83. //悬浮框点击事件
  84. mPreviewLayout.setOnClickListener(new View.OnClickListener() {
  85. @Override
  86. public void onClick(View v) {
  87. Intent intent = new Intent(FloatVideoWindowService.this, VideoChatViewActivity.class);
  88. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  89. startActivity(intent);
  90. mPreviewLayout.removeView(remoteView);
  91. stopSelf();
  92. }
  93. });
  94. //悬浮框触摸事件,设置悬浮框可拖动
  95. mPreviewLayout.setOnTouchListener(new FloatingListener());
  96. }
  97. /** * 开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标) */
  98. private int mTouchStartX;
  99. private int mTouchStartY;
  100. /** * 开始时的坐标和结束时的坐标(相对于自身控件的坐标) */
  101. private int mStartX;
  102. private int mStartY;
  103. /** * 判断悬浮窗口是否移动,这里做个标记,防止移动后松手触发了点击事件 */
  104. private boolean isMove;
  105. private class FloatingListener implements View.OnTouchListener {
  106. int slop = 1;//滑动距离,区分点击
  107. public FloatingListener() {
  108. slop = ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop();
  109. }
  110. @Override
  111. public boolean onTouch(View v, MotionEvent event) {
  112. int action = event.getAction();
  113. switch (action) {
  114. case MotionEvent.ACTION_DOWN:
  115. isMove = false;
  116. mTouchStartX = (int) event.getRawX();
  117. mTouchStartY = (int) event.getRawY();
  118. mStartX = (int) event.getX();
  119. mStartY = (int) event.getY();
  120. break;
  121. case MotionEvent.ACTION_MOVE:
  122. int mTouchCurrentX = (int) event.getRawX();
  123. int mTouchCurrentY = (int) event.getRawY();
  124. wmParams.x -= mTouchCurrentX - mTouchStartX;
  125. wmParams.y += mTouchCurrentY - mTouchStartY;
  126. mWindowManager.updateViewLayout(mFloatingLayout, wmParams);
  127. mTouchStartX = mTouchCurrentX;
  128. mTouchStartY = mTouchCurrentY;
  129. break;
  130. case MotionEvent.ACTION_UP:
  131. int mStopX = (int) event.getX();
  132. int mStopY = (int) event.getY();
  133. if (Math.abs(mStartX - mStopX) >= slop || Math.abs(mStartY - mStopY) >= slop) {
  134. isMove = true;
  135. }
  136. break;
  137. default:
  138. break;
  139. }
  140. //如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件
  141. return isMove;
  142. }
  143. }
  144. }

其中VideoActivity中代码:

  1. private int PermissionRequestCode = 10;
  2. private void onClickFloatBtn() {
  3. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
  4. startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), PermissionRequestCode);
  5. } else {
  6. showFloatView();
  7. }
  8. }
  9. private void showFloatView() {
  10. if (mUid != -1) {
  11. moveTaskToBack(true);
  12. Intent intent = new Intent(this, FloatVideoWindowService.class);
  13. intent.putExtra(KEY_C_USER_ID, mUid);
  14. startService(intent);
  15. }
  16. }
  17. @Override
  18. protected void onActivityResult(final int requestCode, int resultCode, Intent data) {
  19. super.onActivityResult(requestCode, resultCode, data);
  20. if (requestCode == PermissionRequestCode) {
  21. if (mHandler == null) {
  22. mHandler = new Handler(Looper.getMainLooper());
  23. }
  24. //此处特意延时500ms,否则回调值不准确, https://blog.csdn.net/qq_24179679/article/details/84139408
  25. mHandler.postDelayed(new Runnable() {
  26. @Override
  27. public void run() {
  28. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  29. if (Settings.canDrawOverlays(mContext)) { //开启
  30. showFloatView();
  31. } else { //关闭
  32. ToastUtil.show(mContext, "您未授权悬浮窗");
  33. }
  34. }
  35. }
  36. }, 500);
  37. }
  38. }

等等,你刚才坑一里不是说,onActivityResult不回调吗?咋还在singleInstance的页面重写这个方法。经测试,onRequestPermissionsResult的确不回调,但是onActivityResult还是有回调的,延时500ms,回调值更准确哦。

是不是很开森,万事大吉了。人生就像登山,往上走即使一小步也有新高度;然而编程就像玩俄罗斯套娃,打破一小层,还有更大层等着你。走不完的套路,爬不完的坑。

问题描述:当singleInstance页面在onResume时,按下home键,再次点击app的桌面icon,进入后,发现不是刚才的singleInstance页面。我太难啦!!!

有人说可以设置:alwaysRetainTaskState = true

咦~ 这是什么属性?来看看谷歌官方文档怎么说的:

android:alwaysRetainTaskState
系统是否始终保持 Activity 所在任务的状态 —“true”表示是,“false”表示允许系统在特定情况下将任务重置到其初始状态。
默认值为“false”。该属性只对任务的根Activity 有意义;所有其他 Activity 均可忽略该属性。
正常情况下,当用户从主屏幕重新选择某个任务时,系统会在特定情况下清除该任务(从根 Activity 上的堆栈中移除所有Activity)。通常,如果用户在一段时间(如 30 分钟)内未访问任务,系统会执行此操作。
不过,如果该属性的值是“true”,则无论用户如何返回任务,该任务始终会显示最后一次的状态。例如,该属性非常适用于网络浏览器这类应用,因为其中存在大量用户不愿丢失的状态(如多个打开的标签)。

看完最后一句,“该属性非常适用于网络浏览器这类应用,因为其中存在大量用户不愿丢失的状态(如多个打开的标签)”,好像有点那个意思哦,赶紧加上试试,RUN。。。。。满怀期待。。。。虔诚等待。。。。

等了那么久,然并卵。显然是singleInstance在作祟,烦屎了。。。。

剩下我知识储量的最后一招了,纯属无奈之举!
监听app从后台切回前台时机,切回前台时,如果VideoActivity尚在且在通话中,二话不说,直接startActivity之。
怎么监听APP从后台切到前台了呢?注册Application.ActivityLifecycleCallbacks
记得在MyApplication的onCreate中调用注册方法registerActivityLifecycleCallback(new AppActivityLifecycleCallbacks());

看看AppActivityLifecycleCallbacks实现类的代码:

  1. @Override
  2. public void onActivityStarted(Activity activity) {
  3. //视频面试时APP切换至后台,然后由后台切换至前台,视频面试中,且不是悬浮窗显示时 要回到视频面试页面
  4. if (onActivityStoppedFlag && BackgroundUtils.isForeground(activity)
  5. && ActivityStackHelper.isActivityRunning(VideoChatViewActivity.class)
  6. && !SystemUtils.isServiceRunning(activity, FloatVideoWindowService.class.getName())) {
  7. activity.startActivity(new Intent(activity, VideoChatViewActivity.class));
  8. }
  9. onActivityStoppedFlag = !BackgroundUtils.isForeground(activity);
  10. }
  11. /** * 用于判断是否由后台切换至前台 */
  12. private boolean onActivityStoppedFlag = false;
  13. @Override
  14. public void onActivityStopped(Activity activity) {
  15. onActivityStoppedFlag = !BackgroundUtils.isForeground(activity);
  16. }

功能算是“顺利”实现了,但是监听app后台切前台纯属临时方案;恳请有爱的大神帮忙支招,在我的知识盲区指津。

参考文献:
https://www.jianshu.com/p/3786653f9c9b
https://blog.csdn.net/qq\_24179679/article/details/84139408

发表评论

表情:
评论列表 (有 0 条评论,91人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Android悬浮

    hello,i’m Shendi 在Android中想要创建悬浮窗分为三步 1.申请权限 2.使用服务启动悬浮窗 3.设置悬浮窗参数并添加进WindowManag