Android實(shí)現(xiàn)通話最小化懸浮框效果
大家在使用主流的視頻軟件以及直播軟件的時(shí)候,經(jīng)常會(huì)看到打開視頻最小化以后,不是直接關(guān)閉,而是在屏幕右下角一個(gè)小窗口的樣子,本次小編就給大家?guī)淼氖怯肁ndroid實(shí)現(xiàn)在視頻或者語音通話的時(shí)候,最小化也是出現(xiàn)一個(gè)懸浮框的效果。
關(guān)于音視頻通話過程中最小化成懸浮框這個(gè)功能的實(shí)現(xiàn),網(wǎng)絡(luò)上類似的文章很多,但是好像還沒看到解釋的較為清晰的,這里因?yàn)轫?xiàng)目需要實(shí)現(xiàn)了這樣的一個(gè)功能,今天我把它記錄下來,一方面為了以后用到便于自己查閱,一方面也給有需要的人提供一個(gè)思路,讓大家少走彎路。這里我也是參考了些有關(guān)Android懸浮框的文章,再結(jié)合自己的理解所實(shí)現(xiàn)出來的,可能實(shí)現(xiàn)的方法不是最好,但是這或許也是一個(gè)可行的方案。
一、實(shí)現(xiàn)效果(gif效果可能錄制的不是特別好)
二、實(shí)現(xiàn)思路
關(guān)于這個(gè)功能的實(shí)現(xiàn)其實(shí)不難,這里我把實(shí)現(xiàn)思路拆分為了兩步:1、視頻通話Activity的最小化。 2、視頻通話懸浮框的開啟
具體思路是這樣的:當(dāng)用戶點(diǎn)擊最小化按鈕的時(shí)候,最小化我們的視頻通話Activity(這時(shí)Activity處于后臺(tái)狀態(tài)),移除原先在Activity的視頻畫布(因?yàn)槲矣玫氖蔷W(wǎng)易云信,這里他們只能允許一個(gè)視頻畫布存在,這里看情況要不要移除),于此同時(shí),延時(shí)個(gè)幾百毫秒,開啟懸浮框,新建一個(gè)新的視頻畫布然后動(dòng)態(tài)添加到懸浮框里面去,監(jiān)聽?wèi)腋】虻挠|摸事件,讓懸浮框可以拖拽移動(dòng);監(jiān)聽?wèi)腋】虻狞c(diǎn)擊事件,如果用戶點(diǎn)擊了懸浮框,則移除懸浮框里面新建的那個(gè)視頻畫布,然后重新調(diào)起我們?cè)诤笈_(tái)的視頻通話Activity,緊接著新建一個(gè)新的視頻畫布重新動(dòng)態(tài)的添加到Activity里面去。關(guān)于視頻畫布的添加移除方法,這里要看一下所接入的第三方SDK,如用的若是網(wǎng)易云信的SDK,他們的方法如下(下面摘自他們的SDK說明文檔),也就是說移除畫布我只需要傳入null就行了。
1.Activity是如何實(shí)現(xiàn)最小化的?
Activity最小化可能你沒有聽過,但是只要姿勢對(duì)的話,其實(shí)實(shí)現(xiàn)起來非常簡單,因?yàn)锳ctivity本身就自帶了一個(gè)moveTaskToBack(boolean nonRoot),如果我們要實(shí)現(xiàn)最小化,只需要調(diào)用moveTaskToBack(true)傳入一個(gè)true值就可以了,但是這里有一個(gè)前提,就是需要設(shè)置Activity的啟動(dòng)模式為singleInstance模式,兩步搞定。(注:這里先記住一個(gè)小知識(shí)點(diǎn),就是activity最小化后重新從后臺(tái)回到前臺(tái)會(huì)回調(diào)onRestart()方法)
@Override public boolean moveTaskToBack(boolean nonRoot) { return super.moveTaskToBack(nonRoot); }
2.懸浮框是如何開啟的?
這里我把懸浮框的實(shí)現(xiàn)方法寫在一個(gè)服務(wù)Service里面,將懸浮框的開啟關(guān)閉與服務(wù)Service的綁定解綁所關(guān)聯(lián)起來,開啟服務(wù)即相當(dāng)于開啟我們的懸浮框,解綁服務(wù)則相當(dāng)于關(guān)閉關(guān)閉的懸浮框,以此來達(dá)到更好的控制效果。
a. 首先我們聲明一個(gè)服務(wù)類,取名為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. 為懸浮框建立一個(gè)布局文件alert_float_video_layout,這里根據(jù)需求去寫,如果只是像我上面gif那樣,只需要懸浮框顯示對(duì)方的視頻畫布,那么布局文件可以如下所示:(其中懸浮框大小我這里固定為長80dp,高110dp,id為small_size_preview的Linearlayout主要是一個(gè)容器,可以動(dòng)態(tài)的添加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. 布局定義好后,接下來就要對(duì)懸浮框做一些初始化操作了,初始化操作這里我們放在服務(wù)的onCreate()生命周期里面執(zhí)行,因?yàn)橹恍枰獔?zhí)行一次就行了。這里的初始化主要包括對(duì):懸浮框的基本參數(shù)(位置,寬高等),懸浮框的點(diǎn)擊事件以及懸浮框的觸摸事件(即可拖動(dòng)范圍)等的設(shè)置,代碼注釋已經(jīng)很清楚,直接看代碼,如下所示:
public class FloatVideoWindowService extends Service { private WindowManager mWindowManager; private WindowManager.LayoutParams wmParams; private LayoutInflater inflater; //constant private boolean clickflag; //view private View mFloatingLayout; //浮動(dòng)布局 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();//設(shè)置懸浮窗基本參數(shù)(位置、寬高等) initFloating();//懸浮框點(diǎn)擊事件的處理 } @Override public int onStartCommand(Intent intent, int flags, int startId) { return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); } /** * 設(shè)置懸浮框基本參數(shù)(位置、寬高等) */ private void initWindow() { mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); wmParams = getParams();//設(shè)置好懸浮窗的參數(shù) // 懸浮窗默認(rèn)顯示以左上角為起始坐標(biāo) wmParams.gravity = Gravity.LEFT | Gravity.TOP; //懸浮窗的開始位置,因?yàn)樵O(shè)置的是從左上角開始,所以屏幕左上角是x=0;y=0 wmParams.x = 70; wmParams.y = 210; //得到容器,通過這個(gè)inflater來獲得懸浮窗控件 inflater = LayoutInflater.from(getApplicationContext()); // 獲取浮動(dòng)窗口視圖所在布局 mFloatingLayout = inflater.inflate(R.layout.alert_float_video_layout, null); // 添加懸浮窗的視圖 mWindowManager.addView(mFloatingLayout, wmParams); } private WindowManager.LayoutParams getParams() { wmParams = new WindowManager.LayoutParams(); //設(shè)置window type 下面變量2002是在屏幕區(qū)域顯示,2003則可以顯示在狀態(tài)欄之上 wmParams.type = WindowManager.LayoutParams.TYPE_TOAST; //設(shè)置可以顯示在狀態(tài)欄上 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; //設(shè)置懸浮窗口長寬數(shù)據(jù) 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); //懸浮框點(diǎn)擊事件 smallSizePreviewLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //在這里實(shí)現(xiàn)點(diǎn)擊重新回到Activity } }); //懸浮框觸摸事件,設(shè)置懸浮框可拖動(dòng) smallSizePreviewLayout.setOnTouchListener(new FloatingListener()); } //開始觸控的坐標(biāo),移動(dòng)時(shí)的坐標(biāo)(相對(duì)于屏幕左上角的坐標(biāo)) private int mTouchStartX, mTouchStartY, mTouchCurrentX, mTouchCurrentY; //開始時(shí)的坐標(biāo)和結(jié)束時(shí)的坐標(biāo)(相對(duì)于自身控件的坐標(biāo)) private int mStartX, mStartY, mStopX, mStopY; //判斷懸浮窗口是否移動(dòng),這里做個(gè)標(biāo)記,防止移動(dòng)后松手觸發(fā)了點(diǎn)擊事件 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; } //如果是移動(dòng)事件不觸發(fā)OnClick事件,防止移動(dòng)的時(shí)候一放手形成點(diǎn)擊事件 return isMove; } } }
d. 在懸浮框成功被初始化以及相關(guān)參數(shù)被設(shè)置后,接下來就需要將對(duì)方的視頻畫布添加到懸浮框里面去了,這樣我們才能看到對(duì)方的視頻畫面嘛,同樣我們是在Service的oncreate這個(gè)生命周期完成這個(gè)操作的,這里視頻畫布的添加方式使用的網(wǎng)易云信的SDK,具體的添加方式視不同的SDK而定,代碼如下所示:
/** * 初始化預(yù)覽窗口 */ 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. 我們上面說到要將服務(wù)service的綁定與解綁與懸浮框的開啟和關(guān)閉相結(jié)合,所以既然我們?cè)诜?wù)的oncreate()方法中開啟了懸浮框,那么就應(yīng)該在其ondestroy()方法中對(duì)懸浮框進(jìn)行關(guān)閉,關(guān)閉懸浮框的本質(zhì)是將相關(guān)view給移除掉,接著清除我們的視頻畫布,在服務(wù)的ondestroy()方法中執(zhí)行如下代碼:
@Override public void onDestroy() { super.onDestroy(); if (mFloatingLayout != null) { // 移除懸浮窗口 mWindowManager.removeView(mFloatingLayout); } //清除視頻畫布 AVChatManager.getInstance().setupRemoteVideoRender(account, null, false, 0); }
f. 服務(wù)的綁定方式有bindService和startService兩種,使用不同的綁定方式其生命周期也會(huì)不一樣,已知我們需要讓懸浮框在視頻通話activity finish掉的時(shí)候也順便關(guān)掉,那么理所當(dāng)然我們就應(yīng)該采用bind方式來啟動(dòng)服務(wù),讓他的生命周期跟隨他的開啟者,也即是跟隨開啟它的activity生命周期。
intent = new Intent(this, FloatVideoWindowService.class);//開啟服務(wù)顯示懸浮框 bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE); ServiceConnection mVideoServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { // 獲取服務(wù)的操作對(duì)象 FloatVideoWindowService.MyBinder binder = (FloatVideoWindowService.MyBinder) service; binder.getService(); } @Override public void onServiceDisconnected(ComponentName name) { } };
三、完整的流程
現(xiàn)在我們將上面所說的給串聯(lián)起來,思路會(huì)更加清晰一點(diǎn),假設(shè)現(xiàn)在我正在進(jìn)行視頻通話,點(diǎn)擊視頻最小化按鈕,我們應(yīng)該按順序執(zhí)行如下步驟:(如果你姿勢對(duì)的話,現(xiàn)在應(yīng)該是會(huì)出現(xiàn)個(gè)懸浮框了)
public void startVideoService() { moveTaskToBack(true);//最小化Activity intent = new Intent(this, FloatVideoWindowService.class);//開啟服務(wù)顯示懸浮框 bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE); }
當(dāng)我們點(diǎn)擊懸浮框的時(shí)候,可以使用startActivity(intent)來再次打開我們的activity,這時(shí)候視頻通話activity會(huì)回調(diào)onRestart()方法,我們?cè)趏nRestart()生命周期里面unbind解綁掉懸浮框服務(wù),并且重新設(shè)置新的視頻畫布到activity上
@Override protected void onRestart() { super.onRestart(); unbindService(mVideoServiceConnection);//不顯示懸浮框 //從懸浮窗進(jìn)來后重新設(shè)置畫布(判斷是不是接通了) if (isCallEstablished) { //如果接通,先清除所有畫布 avChatUI.clearAllSurfaceView(avChatUI.getAccount()); //延遲重新加載遠(yuǎn)端和本地的視頻畫布 mHandler.postDelayed(new Runnable() { @Override public void run() { avChatUI.initAllSurfaceView(avChatUI.getAccount()); } }, 800); } else { //如果沒接通,直接初始化所有畫布 avChatUI.initLargeSurfaceView(IMCache.getAccount()); } }
以上就是本次為大家分享的關(guān)于Android開發(fā)的又一功能實(shí)現(xiàn)方式,希望我們整理的能夠幫助到你。
相關(guān)文章
輕松實(shí)現(xiàn)Android自定義九宮格圖案解鎖
這篇文章主要幫助大家輕松實(shí)現(xiàn)Android九宮格圖案解鎖功能,可以將圖案轉(zhuǎn)化成數(shù)字密碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11RecyclerView實(shí)現(xiàn)常見的列表菜單
這篇文章主要為大家詳細(xì)介紹了用RecyclerView實(shí)現(xiàn)移動(dòng)應(yīng)用中常見的列表菜單,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12Android多點(diǎn)觸控技術(shù)實(shí)戰(zhàn) 針對(duì)圖片自由縮放和移動(dòng)
這篇文章主要為大家詳細(xì)介紹了Android多點(diǎn)觸控技術(shù)實(shí)戰(zhàn),自由地對(duì)圖片進(jìn)行縮放和移動(dòng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10Android開發(fā)之緩沖dialog對(duì)話框創(chuàng)建、使用與封裝操作
這篇文章主要介紹了Android開發(fā)之緩沖dialog對(duì)話框創(chuàng)建、使用與封裝操作,結(jié)合具體實(shí)例形式分析了Android緩沖dialog對(duì)話框的創(chuàng)建、設(shè)置、顯示、關(guān)閉等操作實(shí)現(xiàn)方法,需要的朋友可以參考下2017-09-09Android中的sqlite查詢數(shù)據(jù)時(shí)去掉重復(fù)值的方法實(shí)例
今天小編就為大家分享一篇關(guān)于Android中的sqlite查詢數(shù)據(jù)時(shí)去掉重復(fù)值的方法實(shí)例,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-01-01Android 使用 DowanloadManager 實(shí)現(xiàn)下載并獲取下載進(jìn)度實(shí)例代碼
這篇文章主要介紹了Android 使用 DowanloadManager 實(shí)現(xiàn)下載并獲取下載進(jìn)度實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-06-06Android app會(huì)crash的原因及解決方法
這篇文章主要介紹了Android app會(huì)crash的原因及解決方法,幫助大家更好的進(jìn)行Android開發(fā),感興趣的朋友可以了解下2020-12-12android 自定義TabActivity的實(shí)例方法
系統(tǒng)自帶的TabActivity的效果不甚理想。開發(fā)中對(duì)TabActivity自定義可能有兩種:第一種:改變TAB行的位置,如放到頁面下方。第二種:對(duì)TabHost圖片的自定義2013-11-11android 仿微信demo——微信啟動(dòng)界面實(shí)現(xiàn)
本篇文章主要介紹了微信小程序-閱讀小程序?qū)嵗╠emo),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,希望能給你們提供幫助2021-06-06