關於音視頻通話過程當中最小化成懸浮框這個功能的實現,網絡上相似的文章不少,可是好像還沒看到解釋的較爲清晰的,這裏由於項目須要實現了這樣的一個功能,今天我把它記錄下來,一方面爲了之後用到便於本身查閱,一方面也給有須要的人提供一個思路,讓你們少走彎路。這裏我也是參考了些有關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