您好,登錄后才能下訂單哦!
1、懸浮窗的基本介紹
懸浮窗,大家應該也不陌生,凌駕于應用之上的一個小彈窗,實現上很簡單,就是添加一個系統級別的窗口,Android中通過WindowManagerService( WMS)來管理所有的窗口,對于WMS來說,管你是Activity、Toast、Dialog,都不過是通過WindowManagerGlobal.addView()添加的一個個View。
Android中的窗口分為三個級別:
1.1 應用窗口,比如Activity的窗口;
1.2 子窗口,依賴于父窗口,比如PopupWindow;
1.3 系統窗口,比如狀態欄、Toast,目標懸浮窗就是系統窗口.
2、根據產品需求進行設計
先了解一下大概的產品需求:
1、懸浮窗需要跨越整個應用
2、需要與懸浮窗進行交互
3、懸浮窗得移動
4、點擊跳轉特定的頁面
5、消息提示的拖拽小紅點
需求很簡單,但是如果估算沒錯,不下一周產品經理會添加新的需求,所以為了更好的后續擴展,需要進行合理的設計,主要分為以下幾點:
1、懸浮窗自定義一個FrameLayout布局FloatLayout,里面進行拖動及點擊響應處理;
2、FloatMonkService,是一個服務,開啟服務的時候創建懸浮窗;
3、FloatCallBack,交互接口,在FloatMonkService里面實現接口,用于交互;
4、FloatWindowManager,懸浮窗的管理,因為后續懸浮窗布局可能有好幾個,可以在這里面進行切換;
5、HomeWatcherReceiver,廣播接收者,因為在應用內展示,需要監聽用戶在點擊Home鍵和切換鍵的時候隱藏懸浮窗,需要FloatMonkService里頭動態注冊;
6、FloatActionController,其實就是代理,其它模塊需要通過它來和懸浮窗進行交互,真正干活的是實現FloatCallBack接口的FloatMonkService;
7、FloatPermissionManager,需要適配各個傻逼機型的權限,慶幸網上已有大佬分享,只需要單獨對7.0系統進行一些適配就行,懸浮窗權限適配;
8、拖拽控件DraggableFlagView,直接拿來在懸浮窗上出現很奇怪的問題,所以需要改造一下下才能達到圖中效果。
3、具體實現
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>
簡單的布局,就是一張圖片+右上角放一個自定義的小紅點。
FloatLayout.java
@Override public boolean onTouchEvent(MotionEvent event) { // 獲取相對屏幕的坐標,即以屏幕左上角為原點 int x = (int) event.getRawX(); int y = (int) event.getRawY(); //下面的這些事件,跟圖標的移動無關,為了區分開拖動和點擊事件 int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: startTime = System.currentTimeMillis(); mTouchStartX = event.getX(); mTouchStartY = event.getY(); break; case MotionEvent.ACTION_MOVE: //圖標移動的邏輯在這里 float mMoveStartX = event.getX(); float mMoveStartY = event.getY(); // 如果移動量大于3才移動 if (Math.abs(mTouchStartX - mMoveStartX) > 3 && Math.abs(mTouchStartY - mMoveStartY) > 3) { // 更新浮動窗口位置參數 mWmParams.x = (int) (x - mTouchStartX); mWmParams.y = (int) (y - mTouchStartY); mWindowManager.updateViewLayout(this, mWmParams); return false; } break; case MotionEvent.ACTION_UP: endTime = System.currentTimeMillis(); //當從點擊到彈起小于半秒的時候,則判斷為點擊,如果超過則不響應點擊事件 if ((endTime - startTime) > 0.1 * 1000L) { isclick = false; } else { isclick = true; } break; } //響應點擊事件 if (isclick) { Toast.makeText(mContext, "我是大傻叼", Toast.LENGTH_SHORT).show(); } return true; }
為了把懸浮窗的view操作抽離出來,自定義了這個布局,主要進行兩部分功能,懸浮窗的移動和點擊處理,重點是通過mWindowManager.updateViewLayout(this, mWmParams)來進行懸浮窗的位置移動,我這個Demo里面只是簡單的通過時間來判斷點擊事件,有必要的話點擊事件需要添加特定View范圍判斷來響應點擊。
// 如果移動量大于3才移動 if (Math.abs(mTouchStartX - mMoveStartX) > 3 && Math.abs(mTouchStartY - mMoveStartY) > 3)
這個判斷是為了避免點擊懸浮窗不在重心位置會出現移動的現象。
FloatMonkService.java
/** * 懸浮窗在服務中創建,通過暴露接口FloatCallBack與Activity進行交互 */ public class FloatMonkService extends Service implements FloatCallBack { /** * home鍵監聽 */ private HomeWatcherReceiver mHomeKeyReceiver; @Override public void onCreate() { super.onCreate(); FloatActionController.getInstance().registerCallLittleMonk(this); //注冊廣播接收者 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); } } /////////////////////////////////////////////////////////實現接口//////////////////////////////////////////////////// @Override public void guideUser(int type) { FloatWindowManager.updataRedAndDialog(this); } /** * 懸浮窗的隱藏 */ @Override public void hide() { FloatWindowManager.hide(); } /** * 懸浮窗的顯示 */ @Override public void show() { FloatWindowManager.show(); } /** * 添加可領取的數量 */ @Override public void addObtainNumer() { FloatWindowManager.addObtainNumer(this); guideUser(4); } /** * 減少可領取的數量 */ @Override public void setObtainNumber(int number) { FloatWindowManager.setObtainNumber(this, number); } }
服務開啟的時候通過FloatWindowManager.createFloatWindow(this)來創建懸浮窗,實現FloatCallBack 實現需要交互的接口。下面看一下創建懸浮窗的真正操作是怎樣的。
FloatWindowManager.java
/** * 創建一個小懸浮窗。初始位置為屏幕的右下角位置。 */ 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之后的用戶不必再去手動開啟懸浮窗權限*/ 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; } } //設置圖片格式,效果為背景透明 wmParams.format = PixelFormat.RGBA_8888; //設置浮動窗口不可聚焦(實現操作除浮動窗口外的其他可見窗口的操作) wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; //調整懸浮窗顯示的停靠位置為左側置頂 wmParams.gravity = Gravity.START | Gravity.TOP; DisplayMetrics dm = new DisplayMetrics(); //取得窗口屬性 mWindowManager.getDefaultDisplay().getMetrics(dm); //窗口的寬度 int screenWidth = dm.widthPixels; //窗口高度 int screenHeight = dm.heightPixels; //以屏幕左上角為原點,設置x、y初始值,相對于gravity wmParams.x = screenWidth; wmParams.y = screenHeight; //設置懸浮窗口長寬數據 wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT; wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT; mFloatLayout.setParams(wmParams); windowManager.addView(mFloatLayout, wmParams); mHasShown = true; //是否展示小紅點展示 checkRedDot(context); } /** * 返回當前已創建的WindowManager。 */ private static WindowManager getWindowManager(Context context) { if (mWindowManager == null) { mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); } return mWindowManager; }
核心代碼其實就是mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE),其中的context不能是Activity的,一開始就說了,Activity會返回它專享的WindowManager,而Activity的窗口級別是屬于應用層的。進行一些初始化操作之后 windowManager.addView(mFloatLayout, wmParams)把布局添加進去就ok了。
if (Build.VERSION.SDK_INT >= 24) { /*android7.0不能用TYPE_TOAST*/ wmParams.type = WindowManager.LayoutParams.TYPE_PHONE; } else { /*以下代碼塊使得android6.0之后的用戶不必再去手動開啟懸浮窗權限*/ 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; } }
說一下這段代碼的意義,當WindowManager.LayoutParams.type設置為WindowManager.LayoutParams.TYPE_TOAST的時候,是可以跳過權限申請的,但是為毛又單獨適配各個機型呢,因為我們有小米Android系統,魅族Android系統,還有華為等等Android系統,特別是產品經理的魅族,一些特殊機型上是沒有效果的,所以為了更保險,得再加一份權限申請,還有一點得提一下,那就是7.0上WindowManager.LayoutParams.TYPE_TOAST,懸浮窗只能持續一秒的時間,所以7.0不設這個type,谷歌爸爸最叼,7.0以上老老實實申請權限。
FloatActionController.java
/** * Author:xishuang * Date:2017.08.01 * Des:與懸浮窗交互的控制類,真正的實現邏輯不在這 */ public class FloatActionController { private FloatActionController() { } public static FloatActionController getInstance() { return LittleMonkProviderHolder.sInstance; } // 靜態內部類 private static class LittleMonkProviderHolder { private static final FloatActionController sInstance = new FloatActionController(); } private FloatCallBack mCallLittleMonk; /** * 開啟服務懸浮窗 */ public void startMonkServer(Context context) { Intent intent = new Intent(context, FloatMonkService.class); context.startService(intent); } /** * 關閉懸浮窗 */ public void stopMonkServer(Context context) { Intent intent = new Intent(context, FloatMonkService.class); context.stopService(intent); } /** * 注冊監聽 */ 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(); } }
這就是暴露出來的接口,按需添加,效果大概是這樣的。
大概效果如下:
Demo:代碼地址感興趣可以看看完整的。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。