Android應(yīng)用內(nèi)懸浮窗的實(shí)現(xiàn)方案示例
1、懸浮窗的基本介紹
懸浮窗,大家應(yīng)該也不陌生,凌駕于應(yīng)用之上的一個(gè)小彈窗,實(shí)現(xiàn)上很簡(jiǎn)單,就是添加一個(gè)系統(tǒng)級(jí)別的窗口,Android中通過(guò)WindowManagerService( WMS)來(lái)管理所有的窗口,對(duì)于WMS來(lái)說(shuō),管你是Activity、Toast、Dialog,都不過(guò)是通過(guò)WindowManagerGlobal.addView()添加的一個(gè)個(gè)View。
Android中的窗口分為三個(gè)級(jí)別:
1.1 應(yīng)用窗口,比如Activity的窗口;
1.2 子窗口,依賴于父窗口,比如PopupWindow;
1.3 系統(tǒng)窗口,比如狀態(tài)欄、Toast,目標(biāo)懸浮窗就是系統(tǒng)窗口.
2、根據(jù)產(chǎn)品需求進(jìn)行設(shè)計(jì)
先了解一下大概的產(chǎn)品需求:
1、懸浮窗需要跨越整個(gè)應(yīng)用
2、需要與懸浮窗進(jìn)行交互
3、懸浮窗得移動(dòng)
4、點(diǎn)擊跳轉(zhuǎn)特定的頁(yè)面
5、消息提示的拖拽小紅點(diǎn)
需求很簡(jiǎn)單,但是如果估算沒(méi)錯(cuò),不下一周產(chǎn)品經(jīng)理會(huì)添加新的需求,所以為了更好的后續(xù)擴(kuò)展,需要進(jìn)行合理的設(shè)計(jì),主要分為以下幾點(diǎn):
1、懸浮窗自定義一個(gè)FrameLayout布局FloatLayout,里面進(jìn)行拖動(dòng)及點(diǎn)擊響應(yīng)處理;
2、FloatMonkService,是一個(gè)服務(wù),開啟服務(wù)的時(shí)候創(chuàng)建懸浮窗;
3、FloatCallBack,交互接口,在FloatMonkService里面實(shí)現(xiàn)接口,用于交互;
4、FloatWindowManager,懸浮窗的管理,因?yàn)楹罄m(xù)懸浮窗布局可能有好幾個(gè),可以在這里面進(jìn)行切換;
5、HomeWatcherReceiver,廣播接收者,因?yàn)樵趹?yīng)用內(nèi)展示,需要監(jiān)聽用戶在點(diǎn)擊Home鍵和切換鍵的時(shí)候隱藏懸浮窗,需要FloatMonkService里頭動(dòng)態(tài)注冊(cè);
6、FloatActionController,其實(shí)就是代理,其它模塊需要通過(guò)它來(lái)和懸浮窗進(jìn)行交互,真正干活的是實(shí)現(xiàn)FloatCallBack接口的FloatMonkService;
7、FloatPermissionManager,需要適配各個(gè)傻逼機(jī)型的權(quán)限,慶幸網(wǎng)上已有大佬分享,只需要單獨(dú)對(duì)7.0系統(tǒng)進(jìn)行一些適配就行,懸浮窗權(quán)限適配;
8、拖拽控件DraggableFlagView,直接拿來(lái)在懸浮窗上出現(xiàn)很奇怪的問(wèn)題,所以需要改造一下下才能達(dá)到圖中效果。
3、具體實(shí)現(xiàn)
float_littlemonk_layout.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:dfv="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:orientation="vertical"> <RelativeLayout android:id="@+id/monk_relative_root" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ImageView android:id="@+id/float_id" android:layout_width="70dp" android:layout_height="80dp" android:layout_gravity="center_vertical|end" android:scaleType="center" android:src="@drawable/little_monk" /> </RelativeLayout> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent"> <floatwindow.xishuang.float_lib.view.DraggableFlagView android:id="@+id/main_dfv" android:layout_width="17dp" android:layout_height="17dp" android:layout_gravity="end" dfv:color1="#FF3B30" /> </FrameLayout> </FrameLayout>
簡(jiǎn)單的布局,就是一張圖片+右上角放一個(gè)自定義的小紅點(diǎn)。
FloatLayout.java
@Override public boolean onTouchEvent(MotionEvent event) { // 獲取相對(duì)屏幕的坐標(biāo),即以屏幕左上角為原點(diǎn) int x = (int) event.getRawX(); int y = (int) event.getRawY(); //下面的這些事件,跟圖標(biāo)的移動(dòng)無(wú)關(guān),為了區(qū)分開拖動(dòng)和點(diǎn)擊事件 int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: startTime = System.currentTimeMillis(); mTouchStartX = event.getX(); mTouchStartY = event.getY(); break; case MotionEvent.ACTION_MOVE: //圖標(biāo)移動(dòng)的邏輯在這里 float mMoveStartX = event.getX(); float mMoveStartY = event.getY(); // 如果移動(dòng)量大于3才移動(dòng) if (Math.abs(mTouchStartX - mMoveStartX) > 3 && Math.abs(mTouchStartY - mMoveStartY) > 3) { // 更新浮動(dòng)窗口位置參數(shù) mWmParams.x = (int) (x - mTouchStartX); mWmParams.y = (int) (y - mTouchStartY); mWindowManager.updateViewLayout(this, mWmParams); return false; } break; case MotionEvent.ACTION_UP: endTime = System.currentTimeMillis(); //當(dāng)從點(diǎn)擊到彈起小于半秒的時(shí)候,則判斷為點(diǎn)擊,如果超過(guò)則不響應(yīng)點(diǎn)擊事件 if ((endTime - startTime) > 0.1 * 1000L) { isclick = false; } else { isclick = true; } break; } //響應(yīng)點(diǎn)擊事件 if (isclick) { Toast.makeText(mContext, "我是大傻叼", Toast.LENGTH_SHORT).show(); } return true; }
為了把懸浮窗的view操作抽離出來(lái),自定義了這個(gè)布局,主要進(jìn)行兩部分功能,懸浮窗的移動(dòng)和點(diǎn)擊處理,重點(diǎn)是通過(guò)mWindowManager.updateViewLayout(this, mWmParams)來(lái)進(jìn)行懸浮窗的位置移動(dòng),我這個(gè)Demo里面只是簡(jiǎn)單的通過(guò)時(shí)間來(lái)判斷點(diǎn)擊事件,有必要的話點(diǎn)擊事件需要添加特定View范圍判斷來(lái)響應(yīng)點(diǎn)擊。
// 如果移動(dòng)量大于3才移動(dòng) if (Math.abs(mTouchStartX - mMoveStartX) > 3 && Math.abs(mTouchStartY - mMoveStartY) > 3)
這個(gè)判斷是為了避免點(diǎn)擊懸浮窗不在重心位置會(huì)出現(xiàn)移動(dòng)的現(xiàn)象。
FloatMonkService.java
/** * 懸浮窗在服務(wù)中創(chuàng)建,通過(guò)暴露接口FloatCallBack與Activity進(jìn)行交互 */ public class FloatMonkService extends Service implements FloatCallBack { /** * home鍵監(jiān)聽 */ private HomeWatcherReceiver mHomeKeyReceiver; @Override public void onCreate() { super.onCreate(); FloatActionController.getInstance().registerCallLittleMonk(this); //注冊(cè)廣播接收者 mHomeKeyReceiver = new HomeWatcherReceiver(); final IntentFilter homeFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); registerReceiver(mHomeKeyReceiver, homeFilter); //初始化懸浮窗UI initWindowData(); } @Override public IBinder onBind(Intent intent) { return null; } /** * 初始化WindowManager */ private void initWindowData() { FloatWindowManager.createFloatWindow(this); } @Override public void onDestroy() { super.onDestroy(); //移除懸浮窗 FloatWindowManager.removeFloatWindowManager(); //注銷廣播接收者 if (null != mHomeKeyReceiver) { unregisterReceiver(mHomeKeyReceiver); } } /////////////////////////////////////////////////////////實(shí)現(xiàn)接口//////////////////////////////////////////////////// @Override public void guideUser(int type) { FloatWindowManager.updataRedAndDialog(this); } /** * 懸浮窗的隱藏 */ @Override public void hide() { FloatWindowManager.hide(); } /** * 懸浮窗的顯示 */ @Override public void show() { FloatWindowManager.show(); } /** * 添加可領(lǐng)取的數(shù)量 */ @Override public void addObtainNumer() { FloatWindowManager.addObtainNumer(this); guideUser(4); } /** * 減少可領(lǐng)取的數(shù)量 */ @Override public void setObtainNumber(int number) { FloatWindowManager.setObtainNumber(this, number); } }
服務(wù)開啟的時(shí)候通過(guò)FloatWindowManager.createFloatWindow(this)來(lái)創(chuàng)建懸浮窗,實(shí)現(xiàn)FloatCallBack 實(shí)現(xiàn)需要交互的接口。下面看一下創(chuàng)建懸浮窗的真正操作是怎樣的。
FloatWindowManager.java
/** * 創(chuàng)建一個(gè)小懸浮窗。初始位置為屏幕的右下角位置。 */ public static void createFloatWindow(Context context) { wmParams = new WindowManager.LayoutParams(); WindowManager windowManager = getWindowManager(context); mFloatLayout = new FloatLayout(context); if (Build.VERSION.SDK_INT >= 24) { /*android7.0不能用TYPE_TOAST*/ wmParams.type = WindowManager.LayoutParams.TYPE_PHONE; } else { /*以下代碼塊使得android6.0之后的用戶不必再去手動(dòng)開啟懸浮窗權(quán)限*/ String packname = context.getPackageName(); PackageManager pm = context.getPackageManager(); boolean permission = (PackageManager.PERMISSION_GRANTED == pm.checkPermission("android.permission.SYSTEM_ALERT_WINDOW", packname)); if (permission) { wmParams.type = WindowManager.LayoutParams.TYPE_PHONE; } else { wmParams.type = WindowManager.LayoutParams.TYPE_TOAST; } } //設(shè)置圖片格式,效果為背景透明 wmParams.format = PixelFormat.RGBA_8888; //設(shè)置浮動(dòng)窗口不可聚焦(實(shí)現(xiàn)操作除浮動(dòng)窗口外的其他可見窗口的操作) wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; //調(diào)整懸浮窗顯示的停靠位置為左側(cè)置頂 wmParams.gravity = Gravity.START | Gravity.TOP; DisplayMetrics dm = new DisplayMetrics(); //取得窗口屬性 mWindowManager.getDefaultDisplay().getMetrics(dm); //窗口的寬度 int screenWidth = dm.widthPixels; //窗口高度 int screenHeight = dm.heightPixels; //以屏幕左上角為原點(diǎn),設(shè)置x、y初始值,相對(duì)于gravity wmParams.x = screenWidth; wmParams.y = screenHeight; //設(shè)置懸浮窗口長(zhǎng)寬數(shù)據(jù) wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT; wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT; mFloatLayout.setParams(wmParams); windowManager.addView(mFloatLayout, wmParams); mHasShown = true; //是否展示小紅點(diǎn)展示 checkRedDot(context); } /** * 返回當(dāng)前已創(chuàng)建的WindowManager。 */ private static WindowManager getWindowManager(Context context) { if (mWindowManager == null) { mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); } return mWindowManager; }
核心代碼其實(shí)就是mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE),其中的context不能是Activity的,一開始就說(shuō)了,Activity會(huì)返回它專享的WindowManager,而Activity的窗口級(jí)別是屬于應(yīng)用層的。進(jìn)行一些初始化操作之后 windowManager.addView(mFloatLayout, wmParams)把布局添加進(jìn)去就ok了。
if (Build.VERSION.SDK_INT >= 24) { /*android7.0不能用TYPE_TOAST*/ wmParams.type = WindowManager.LayoutParams.TYPE_PHONE; } else { /*以下代碼塊使得android6.0之后的用戶不必再去手動(dòng)開啟懸浮窗權(quán)限*/ String packname = context.getPackageName(); PackageManager pm = context.getPackageManager(); boolean permission = (PackageManager.PERMISSION_GRANTED == pm.checkPermission("android.permission.SYSTEM_ALERT_WINDOW", packname)); if (permission) { wmParams.type = WindowManager.LayoutParams.TYPE_PHONE; } else { wmParams.type = WindowManager.LayoutParams.TYPE_TOAST; } }
說(shuō)一下這段代碼的意義,當(dāng)WindowManager.LayoutParams.type設(shè)置為WindowManager.LayoutParams.TYPE_TOAST的時(shí)候,是可以跳過(guò)權(quán)限申請(qǐng)的,但是為毛又單獨(dú)適配各個(gè)機(jī)型呢,因?yàn)槲覀冇行∶譇ndroid系統(tǒng),魅族Android系統(tǒng),還有華為等等Android系統(tǒng),特別是產(chǎn)品經(jīng)理的魅族,一些特殊機(jī)型上是沒(méi)有效果的,所以為了更保險(xiǎn),得再加一份權(quán)限申請(qǐng),還有一點(diǎn)得提一下,那就是7.0上WindowManager.LayoutParams.TYPE_TOAST,懸浮窗只能持續(xù)一秒的時(shí)間,所以7.0不設(shè)這個(gè)type,谷歌爸爸最叼,7.0以上老老實(shí)實(shí)申請(qǐng)權(quán)限。
FloatActionController.java
/** * Author:xishuang * Date:2017.08.01 * Des:與懸浮窗交互的控制類,真正的實(shí)現(xiàn)邏輯不在這 */ public class FloatActionController { private FloatActionController() { } public static FloatActionController getInstance() { return LittleMonkProviderHolder.sInstance; } // 靜態(tài)內(nèi)部類 private static class LittleMonkProviderHolder { private static final FloatActionController sInstance = new FloatActionController(); } private FloatCallBack mCallLittleMonk; /** * 開啟服務(wù)懸浮窗 */ public void startMonkServer(Context context) { Intent intent = new Intent(context, FloatMonkService.class); context.startService(intent); } /** * 關(guān)閉懸浮窗 */ public void stopMonkServer(Context context) { Intent intent = new Intent(context, FloatMonkService.class); context.stopService(intent); } /** * 注冊(cè)監(jiān)聽 */ public void registerCallLittleMonk(FloatCallBack callLittleMonk) { mCallLittleMonk = callLittleMonk; } /** * 懸浮窗的顯示 */ public void show() { if (mCallLittleMonk == null) return; mCallLittleMonk.show(); } /** * 懸浮窗的隱藏 */ public void hide() { if (mCallLittleMonk == null) return; mCallLittleMonk.hide(); } }
這就是暴露出來(lái)的接口,按需添加,效果大概是這樣的。
大概效果如下:
Demo:代碼地址感興趣可以看看完整的。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- android 添加隨意拖動(dòng)的桌面懸浮窗口
- Android實(shí)現(xiàn)桌面懸浮窗、蒙板效果實(shí)例代碼
- 不依賴于Activity的Android全局懸浮窗的實(shí)現(xiàn)
- Android 懸浮窗權(quán)限各機(jī)型各系統(tǒng)適配大全(總結(jié))
- Android實(shí)現(xiàn)類似360,QQ管家那樣的懸浮窗
- Android實(shí)現(xiàn)類似qq微信消息懸浮窗通知功能
- Android 8.0如何完美適配全局dialog懸浮窗彈出
- Android懸浮窗屏蔽懸浮窗外部所有的點(diǎn)擊事件的實(shí)例代碼
- Android 獲取判斷是否有懸浮窗權(quán)限的方法
- android仿華為手機(jī)懸浮窗設(shè)計(jì)
相關(guān)文章
Android取消EditText自動(dòng)獲取默認(rèn)焦點(diǎn)
本文主要介紹了Android取消EditText自動(dòng)獲取焦點(diǎn)默認(rèn)行為的方法,具有很好的參考價(jià)值。下面跟著小編一起來(lái)看下吧2017-03-03Android用Scroller實(shí)現(xiàn)一個(gè)可向上滑動(dòng)的底部導(dǎo)航欄
本篇文章主要介紹了Android用Scroller實(shí)現(xiàn)一個(gè)可上滑的底部導(dǎo)航欄,具有一定的參考價(jià)值,有興趣的小伙伴們可以參考一下2017-07-07Android編程實(shí)現(xiàn)異步消息處理機(jī)制的幾種方法總結(jié)
這篇文章主要介紹了Android編程實(shí)現(xiàn)異步消息處理機(jī)制的幾種方法,結(jié)合實(shí)例形式詳細(xì)總結(jié)分析了Android異步消息處理機(jī)制的原理、相關(guān)實(shí)現(xiàn)技巧與操作注意事項(xiàng),需要的朋友可以參考下2018-08-08Flutter 網(wǎng)絡(luò)請(qǐng)求框架封裝詳解
這篇文章主要介紹了Flutter 網(wǎng)絡(luò)請(qǐng)求框架封裝詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03Android之RecyclerView實(shí)現(xiàn)時(shí)光軸效果示例
本篇文章主要介紹了Android之RecyclerView實(shí)現(xiàn)時(shí)光軸效果,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02Android開發(fā)實(shí)現(xiàn)Launcher3應(yīng)用列表修改透明背景的方法
這篇文章主要介紹了Android開發(fā)實(shí)現(xiàn)Launcher3應(yīng)用列表修改透明背景的方法,結(jié)合實(shí)例形式分析了Launcher3相關(guān)配置文件與功能函數(shù)修改設(shè)置操作技巧,需要的朋友可以參考下2017-11-11android自定義開關(guān)控件-SlideSwitch的實(shí)例
本篇文章主要介紹了android自定義開關(guān)控件-SlideSwitch的實(shí)例,實(shí)現(xiàn)了手機(jī)控件開關(guān)的功能,感興趣的小伙伴們可以參考一下。2016-11-11Android中FontMetrics的幾個(gè)屬性全面講解
下面小編就為大家?guī)?lái)一篇Android中FontMetrics的幾個(gè)屬性全面講解。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-11-11很贊的引導(dǎo)界面效果Android控件ImageSwitcher實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了Android控件ImageSwitcher如何實(shí)現(xiàn)很贊的引導(dǎo)界面的具體代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-05-05