文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android悬浮窗视频

2022-06-06 13:10

关注

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

当用户正在NewsActivity看文字,视频通话来了,接听(VideoActivity),然后缩至悬浮窗,此时应回到NewsActivity,悬浮窗出现时该如何回到电话前的页面?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所在的应用

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

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

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

下面是悬浮窗的实现



public class FloatVideoWindowService extends Service {
    private WindowManager mWindowManager;
    private WindowManager.LayoutParams wmParams;
    private View mFloatingLayout;
    private RelativeLayout mPreviewLayout;
    private TutorialsManager mInstance = null;
    private SurfaceView remoteView = null;
    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }
    public class MyBinder extends Binder {
//        public FloatVideoWindowService getService() {
//            return FloatVideoWindowService.this;
//        }
    }
    @Override
    public void onCreate() {
        super.onCreate();
        initWindow();//设置悬浮窗基本参数(位置、宽高等)
        initFloating();//悬浮框点击事件的处理
        mInstance = TutorialsManager.getInstance(this);
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        int uid = intent.getIntExtra(VideoChatViewActivity.KEY_C_USER_ID, -1);
        if (uid != -1) {
            remoteView = mInstance.getRemoteVideo(uid);
            ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            remoteView.setLayoutParams(lp);
            mPreviewLayout.addView(remoteView);
        } else {
            stopSelf();
        }
        return super.onStartCommand(intent, flags, startId);
    }
    @Override
    public void onDestroy() {
        if (null != mPreviewLayout) {
            mPreviewLayout.removeAllViews();
        }
        if (mFloatingLayout != null) {
            // 移除悬浮窗口
            mWindowManager.removeView(mFloatingLayout);
        }
    }
    
    private void initWindow() {
        mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
        wmParams = getParams();
        // 悬浮窗默认显示以左上角为起始坐标
        wmParams.gravity = Gravity.RIGHT | Gravity.TOP;
        //悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0
        wmParams.x = DensityUtil.dip2px(10);
        wmParams.y = 110;
        // 获取浮动窗口视图所在布局
        mFloatingLayout = LayoutInflater.from(getApplicationContext()).inflate(R.layout.view_videochat_services_float_layout, null);
        // 添加悬浮窗的视图
        mWindowManager.addView(mFloatingLayout, wmParams);
    }
    private WindowManager.LayoutParams getParams() {
        wmParams = new WindowManager.LayoutParams();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        } else {
            wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        }
        //设置可以显示在状态栏上
        wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams
                .FLAG_NOT_TOUCH_MODAL |
                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR |
                WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
        int fullCropWidth = DeviceUtils.getScreenWidth();
        int cropHeight = fullCropWidth * 16 / 9;
        //设置悬浮窗口长宽数据
        wmParams.width = (int) (fullCropWidth * 0.26);
        wmParams.height = (int) (cropHeight * 0.26);
        return wmParams;
    }
    private void initFloating() {
        mPreviewLayout = mFloatingLayout.findViewById(R.id.small_size_preview);
        //悬浮框点击事件
        mPreviewLayout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(FloatVideoWindowService.this, VideoChatViewActivity.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(intent);
                mPreviewLayout.removeView(remoteView);
                stopSelf();
            }
        });
        //悬浮框触摸事件,设置悬浮框可拖动
        mPreviewLayout.setOnTouchListener(new FloatingListener());
    }
    
    private int mTouchStartX;
    private int mTouchStartY;
    
    private int mStartX;
    private int mStartY;
    
    private boolean isMove;
    private class FloatingListener implements View.OnTouchListener {
        int slop = 1;//滑动距离,区分点击
        public FloatingListener() {
            slop = ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop();
        }
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    isMove = false;
                    mTouchStartX = (int) event.getRawX();
                    mTouchStartY = (int) event.getRawY();
                    mStartX = (int) event.getX();
                    mStartY = (int) event.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    int mTouchCurrentX = (int) event.getRawX();
                    int mTouchCurrentY = (int) event.getRawY();
                    wmParams.x -= mTouchCurrentX - mTouchStartX;
                    wmParams.y += mTouchCurrentY - mTouchStartY;
                    mWindowManager.updateViewLayout(mFloatingLayout, wmParams);
                    mTouchStartX = mTouchCurrentX;
                    mTouchStartY = mTouchCurrentY;
                    break;
                case MotionEvent.ACTION_UP:
                    int mStopX = (int) event.getX();
                    int mStopY = (int) event.getY();
                    if (Math.abs(mStartX - mStopX) >= slop || Math.abs(mStartY - mStopY) >= slop) {
                        isMove = true;
                    }
                    break;
                default:
                    break;
            }
            //如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件
            return isMove;
        }
    }
}

其中VideoActivity中代码:

    private int PermissionRequestCode = 10;
    private void onClickFloatBtn() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) {
            startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), PermissionRequestCode);
        } else {
            showFloatView();
        }
    }
    private void showFloatView() {
        if (mUid != -1) {
            moveTaskToBack(true);
            Intent intent = new Intent(this, FloatVideoWindowService.class);
            intent.putExtra(KEY_C_USER_ID, mUid);
            startService(intent);
        }
    }
    @Override
    protected void onActivityResult(final int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == PermissionRequestCode) {
            if (mHandler == null) {
                mHandler = new Handler(Looper.getMainLooper());
            }
            //此处特意延时500ms,否则回调值不准确,   https://blog.csdn.net/qq_24179679/article/details/84139408
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                        if (Settings.canDrawOverlays(mContext)) {//开启
                            showFloatView();
                        } else {//关闭
                            ToastUtil.show(mContext, "您未授权悬浮窗");
                        }
                    }
                }
            }, 500);
        }
    }

等等,你刚才坑一里不是说,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实现类的代码:

    @Override
    public void onActivityStarted(Activity activity) {
            //视频面试时APP切换至后台,然后由后台切换至前台,视频面试中,且不是悬浮窗显示时 要回到视频面试页面
            if (onActivityStoppedFlag && BackgroundUtils.isForeground(activity)
                    && ActivityStackHelper.isActivityRunning(VideoChatViewActivity.class)
                    && !SystemUtils.isServiceRunning(activity, FloatVideoWindowService.class.getName())) {
                activity.startActivity(new Intent(activity, VideoChatViewActivity.class));
            }
            onActivityStoppedFlag = !BackgroundUtils.isForeground(activity);
    }
    
    private boolean onActivityStoppedFlag = false;
    @Override
    public void onActivityStopped(Activity activity) {
        onActivityStoppedFlag = !BackgroundUtils.isForeground(activity);
    }

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

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


作者:iblade


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯