Android音视频通话过程当中最小化成悬浮框的实现(相似Android8.0画中画效果)

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

 

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

                   

 

2、实现思路app

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

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

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

Activity最小化可能你没有听过,可是只要姿式对的话,其实实现起来很是简单,由于Activity自己就自带了一个moveTaskToBack(boolean nonRoot),若是咱们要实现最小化,只须要调用moveTaskToBack(true)传入一个true值就能够了,可是这里有一个前提,就是须要设置Activity的启动模式为singleInstance模式,两步搞定。(注:这里先记住一个小知识点,就是activity最小化后从新从后台回到前台会回调onRestart()方法this

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

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

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

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

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) {
        }
    };

 

3、完整的流程

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

   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

相关文章
相关标签/搜索