zoukankan      html  css  js  c++  java
  • Android音视频通话过程中最小化成悬浮框的实现(类似Android8.0画中画效果)

    关于音视频通话过程中最小化成悬浮框这个功能的实现,网络上类似的文章很多,但是好像还没看到解释的较为清晰的,这里因为项目需要实现了这样的一个功能,今天我把它记录下来,一方面为了以后用到便于自己查阅,一方面也给有需要的人提供一个思路,让大家少走弯路。这里我也是参考了些有关Android悬浮框的文章,再结合自己的理解所实现出来的,可能实现的方法不是最好,但是这或许也是一个可行的方案。

    一、实现效果(gif效果可能录制的不是特别好)

                       

    二、实现思路

    关于这个功能的实现其实不难,这里我把实现思路拆分为了两步:1、视频通话Activity的最小化。 2、视频通话悬浮框的开启

    具体思路是这样的:用户点击最小化按钮的时候,最小化我们的视频通话Activity(这时Activity处于后台状态),移除原先在Activity的视频画布(因为我用的是网易云信,这里他们只能允许一个视频画布存在,这里看情况要不要移除),于此同时,延时个几百毫秒,开启悬浮框,新建一个新的视频画布然后动态添加到悬浮框里面去,监听悬浮框的触摸事件,让悬浮框可以拖拽移动;监听悬浮框的点击事件,如果用户点击了悬浮框,则移除悬浮框里面新建的那个视频画布,然后重新调起我们在后台的视频通话Activity,紧接着新建一个新的视频画布重新动态的添加到Activity里面去。关于视频画布的添加移除方法,这里要看一下所接入的第三方SDK,如用的若是网易云信的SDK,他们的方法如下(下面摘自他们的SDK说明文档),也就是说移除画布我只需要传入null就行了。

    1.Activity是如何实现最小化的?

    Activity最小化可能你没有听过,但是只要姿势对的话,其实实现起来非常简单,因为Activity本身就自带了一个moveTaskToBack(boolean nonRoot),如果我们要实现最小化,只需要调用moveTaskToBack(true)传入一个true值就可以了,但是这里有一个前提,就是需要设置Activity的启动模式为singleInstance模式,两步搞定。(注:这里先记住一个小知识点,就是activity最小化后重新从后台回到前台会回调onRestart()方法

        @Override
        public boolean moveTaskToBack(boolean nonRoot) {
            return super.moveTaskToBack(nonRoot);
        }

    2.悬浮框是如何开启的?

    这里我把悬浮框的实现方法写在一个服务Service里面,将悬浮框的开启关闭与服务Service的绑定解绑所关联起来,开启服务即相当于开启我们的悬浮框,解绑服务则相当于关闭关闭的悬浮框,以此来达到更好的控制效果。

    a. 首先我们声明一个服务类,取名为FloatVideoWindowService:

    public class FloatVideoWindowService extends Service {
    
        @Nullable
        @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();
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            return super.onStartCommand(intent, flags, startId);
        }
    
        @Override
        public void onDestroy() {
            super.onDestroy();
        }
    }

    b. 为悬浮框建立一个布局文件alert_float_video_layout,这里根据需求去写,如果只是像我上面gif那样,只需要悬浮框显示对方的视频画布,那么布局文件可以如下所示:(其中悬浮框大小我这里固定为长80dp,高110dp,id为small_size_preview的Linearlayout主要是一个容器,可以动态的添加view到里面去,也就是我们的视频画布)

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    
        <FrameLayout
            android:layout_width="80dp"
            android:layout_height="110dp"
            android:background="@color/black_1f2d3d">
    
            <LinearLayout
                android:id="@+id/small_size_preview"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/transparent"
                android:orientation="vertical" />
        </FrameLayout>
    </LinearLayout>

    c. 布局定义好后,接下来就要对悬浮框做一些初始化操作了,初始化操作这里我们放在服务的onCreate()生命周期里面执行,因为只需要执行一次就行了。这里的初始化主要包括对:悬浮框的基本参数(位置,宽高等),悬浮框的点击事件以及悬浮框的触摸事件(即可拖动范围)等的设置,代码注释已经很清楚,直接看代码,如下所示:

    public class FloatVideoWindowService extends Service {
        private WindowManager mWindowManager;
        private WindowManager.LayoutParams wmParams;
        private LayoutInflater inflater;
    
        //constant
        private boolean clickflag;
    
        //view
        private View mFloatingLayout;    //浮动布局
        private LinearLayout smallSizePreviewLayout; //容器父布局
    
        @Nullable
        @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();//悬浮框点击事件的处理
        }
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            return super.onStartCommand(intent, flags, startId);
        }
    
      
        @Override
        public void onDestroy() {
            super.onDestroy();
        }
    
        /**
         * 设置悬浮框基本参数(位置、宽高等)
         */
        private void initWindow() {
            mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
            wmParams = getParams();//设置好悬浮窗的参数
            // 悬浮窗默认显示以左上角为起始坐标
            wmParams.gravity = Gravity.LEFT | Gravity.TOP;
            //悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0
            wmParams.x = 70;
            wmParams.y = 210;
            //得到容器,通过这个inflater来获得悬浮窗控件
            inflater = LayoutInflater.from(getApplicationContext());
            // 获取浮动窗口视图所在布局
            mFloatingLayout = inflater.inflate(R.layout.alert_float_video_layout, null);
            // 添加悬浮窗的视图
            mWindowManager.addView(mFloatingLayout, wmParams);
        }
    
      
        private WindowManager.LayoutParams getParams() {
            wmParams = new WindowManager.LayoutParams();
            //设置window type 下面变量2002是在屏幕区域显示,2003则可以显示在状态栏之上
            wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;
            //设置可以显示在状态栏上
            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;
    
            //设置悬浮窗口长宽数据
            wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
            wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
            return wmParams;
        }
    
       private void
    initFloating() { smallSizePreviewLayout = mFloatingLayout.findViewById(R.id.small_size_preview); //悬浮框点击事件 smallSizePreviewLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) {  //在这里实现点击重新回到Activity } }); //悬浮框触摸事件,设置悬浮框可拖动 smallSizePreviewLayout.setOnTouchListener(new FloatingListener()); } //开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标) private int mTouchStartX, mTouchStartY, mTouchCurrentX, mTouchCurrentY; //开始时的坐标和结束时的坐标(相对于自身控件的坐标) private int mStartX, mStartY, mStopX, mStopY;
       //判断悬浮窗口是否移动,这里做个标记,防止移动后松手触发了点击事件 private boolean isMove; private class FloatingListener implements
    View.OnTouchListener { @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: mTouchCurrentX = (int) event.getRawX(); 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: mStopX = (int) event.getX(); mStopY = (int) event.getY(); if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) { isMove = true; } break; } //如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件 return isMove; } } }

    d. 在悬浮框成功被初始化以及相关参数被设置后,接下来就需要将对方的视频画布添加到悬浮框里面去了,这样我们才能看到对方的视频画面嘛,同样我们是在Service的oncreate这个生命周期完成这个操作的,这里视频画布的添加方式使用的网易云信的SDK,具体的添加方式视不同的SDK而定,代码如下所示:

        /**
         * 初始化预览窗口
         */
        private void initSurface() {
            if (smallRender == null) {
                smallRender = new AVChatSurfaceViewRenderer(getApplicationContext());
            }
    
            addIntoSmallSizePreviewLayout(smallRender);
        }
    
        /**
         * 添加surfaceview到smallSizePreviewLayout
         */
        private void addIntoSmallSizePreviewLayout(SurfaceView surfaceView) {
            if (surfaceView.getParent() != null) {
                ((ViewGroup) surfaceView.getParent()).removeView(surfaceView);
            }
    
            smallSizePreviewLayout.addView(surfaceView);
            surfaceView.setZOrderMediaOverlay(true);
        }

    e. 我们上面说到要将服务service的绑定与解绑与悬浮框的开启和关闭相结合,所以既然我们在服务的oncreate()方法中开启了悬浮框,那么就应该在其ondestroy()方法中对悬浮框进行关闭,关闭悬浮框的本质是将相关view给移除掉,接着清除我们的视频画布,在服务的ondestroy()方法中执行如下代码:

      @Override
        public void onDestroy() {
            super.onDestroy();
            if (mFloatingLayout != null) {
                // 移除悬浮窗口
                mWindowManager.removeView(mFloatingLayout);
            }
    
            //清除视频画布
            AVChatManager.getInstance().setupRemoteVideoRender(account, null, false, 0);
        }

    f. 服务的绑定方式有bindService和startService两种,使用不同的绑定方式其生命周期也会不一样,已知我们需要让悬浮框在视频通话activity finish掉的时候也顺便关掉,那么理所当然我们就应该采用bind方式来启动服务,让他的生命周期跟随他的开启者,也即是跟随开启它的activity生命周期。

    intent = new Intent(this, FloatVideoWindowService.class);//开启服务显示悬浮框
    bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE);
    
    ServiceConnection mVideoServiceConnection = new ServiceConnection() {
    
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                // 获取服务的操作对象
                FloatVideoWindowService.MyBinder binder = (FloatVideoWindowService.MyBinder) service;
                binder.getService();
            }
    
            @Override
            public void onServiceDisconnected(ComponentName name) {
            }
        };

    三、完整的流程

    现在我们将上面所说的给串联起来,思路会更加清晰一点,假设现在我正在进行视频通话,点击视频最小化按钮,我们应该按顺序执行如下步骤:(如果你姿势对的话,现在应该是会出现个悬浮框了)

       public void startVideoService() {
             moveTaskToBack(true);//最小化Activity
             intent = new Intent(this, FloatVideoWindowService.class);//开启服务显示悬浮框
             bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE);
        }

    当我们点击悬浮框的时候,可以使用startActivity(intent)来再次打开我们的activity,这时候视频通话activity会回调onRestart()方法,我们在onRestart()生命周期里面unbind解绑掉悬浮框服务,并且重新设置新的视频画布到activity上

      @Override
        protected void onRestart() {
            super.onRestart();
            unbindService(mVideoServiceConnection);//不显示悬浮框
    
            //从悬浮窗进来后重新设置画布(判断是不是接通了)
            if (isCallEstablished) {
                //如果接通,先清除所有画布
                avChatUI.clearAllSurfaceView(avChatUI.getAccount());
               //延迟重新加载远端和本地的视频画布
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        avChatUI.initAllSurfaceView(avChatUI.getAccount());
                       
                    }
                }, 800);
            } else {
                //如果没接通,直接初始化所有画布
                avChatUI.initLargeSurfaceView(IMCache.getAccount());
            }
        }                        

    已经很久没有写过博客了,写着写着可能有点乱( ̄_ ̄|||)

    如果有什么疑问或者有更好的实现思路的,欢迎给我留言~

    联系方式:471497226@qq.com

  • 相关阅读:
    期中考试补完计划
    c++简单程序设计-4
    c++简单程序设计-3
    c++简单程序设计-2
    c++简单程序设计-1
    layer ifram 弹出框
    定时发邮件
    二维码生成
    文件或文件夹的压缩
    excel 导入 与 导出
  • 原文地址:https://www.cnblogs.com/cxk1995/p/7824375.html
Copyright © 2011-2022 走看看