溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Android應(yīng)用內(nèi)懸浮窗的實現(xiàn)方案示例

發(fā)布時間:2020-09-04 22:01:01 來源:腳本之家 閱讀:2387 作者:C6C 欄目:移動開發(fā)

1、懸浮窗的基本介紹

懸浮窗,大家應(yīng)該也不陌生,凌駕于應(yīng)用之上的一個小彈窗,實現(xiàn)上很簡單,就是添加一個系統(tǒng)級別的窗口,Android中通過WindowManagerService( WMS)來管理所有的窗口,對于WMS來說,管你是Activity、Toast、Dialog,都不過是通過WindowManagerGlobal.addView()添加的一個個View。

Android中的窗口分為三個級別:

1.1 應(yīng)用窗口,比如Activity的窗口;

1.2 子窗口,依賴于父窗口,比如PopupWindow;

1.3 系統(tǒng)窗口,比如狀態(tài)欄、Toast,目標(biāo)懸浮窗就是系統(tǒng)窗口.

2、根據(jù)產(chǎn)品需求進行設(shè)計

先了解一下大概的產(chǎn)品需求:

1、懸浮窗需要跨越整個應(yīng)用
2、需要與懸浮窗進行交互
3、懸浮窗得移動
4、點擊跳轉(zhuǎn)特定的頁面
5、消息提示的拖拽小紅點

需求很簡單,但是如果估算沒錯,不下一周產(chǎn)品經(jīng)理會添加新的需求,所以為了更好的后續(xù)擴展,需要進行合理的設(shè)計,主要分為以下幾點:

1、懸浮窗自定義一個FrameLayout布局FloatLayout,里面進行拖動及點擊響應(yīng)處理;
2、FloatMonkService,是一個服務(wù),開啟服務(wù)的時候創(chuàng)建懸浮窗;
3、FloatCallBack,交互接口,在FloatMonkService里面實現(xiàn)接口,用于交互;
4、FloatWindowManager,懸浮窗的管理,因為后續(xù)懸浮窗布局可能有好幾個,可以在這里面進行切換;
5、HomeWatcherReceiver,廣播接收者,因為在應(yīng)用內(nèi)展示,需要監(jiān)聽用戶在點擊Home鍵和切換鍵的時候隱藏懸浮窗,需要FloatMonkService里頭動態(tài)注冊;
6、FloatActionController,其實就是代理,其它模塊需要通過它來和懸浮窗進行交互,真正干活的是實現(xiàn)FloatCallBack接口的FloatMonkService;
7、FloatPermissionManager,需要適配各個傻逼機型的權(quán)限,慶幸網(wǎng)上已有大佬分享,只需要單獨對7.0系統(tǒng)進行一些適配就行,懸浮窗權(quán)限適配;
8、拖拽控件DraggableFlagView,直接拿來在懸浮窗上出現(xiàn)很奇怪的問題,所以需要改造一下下才能達到圖中效果。

3、具體實現(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>

簡單的布局,就是一張圖片+右上角放一個自定義的小紅點。

FloatLayout.java

@Override
  public boolean onTouchEvent(MotionEvent event) {
    // 獲取相對屏幕的坐標(biāo),即以屏幕左上角為原點
    int x = (int) event.getRawX();
    int y = (int) event.getRawY();
    //下面的這些事件,跟圖標(biāo)的移動無關(guān),為了區(qū)分開拖動和點擊事件
    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)移動的邏輯在這里
        float mMoveStartX = event.getX();
        float mMoveStartY = event.getY();
        // 如果移動量大于3才移動
        if (Math.abs(mTouchStartX - mMoveStartX) > 3
            && Math.abs(mTouchStartY - mMoveStartY) > 3) {
          // 更新浮動窗口位置參數(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)從點擊到彈起小于半秒的時候,則判斷為點擊,如果超過則不響應(yīng)點擊事件
        if ((endTime - startTime) > 0.1 * 1000L) {
          isclick = false;
        } else {
          isclick = true;
        }
        break;
    }
    //響應(yīng)點擊事件
    if (isclick) {
      Toast.makeText(mContext, "我是大傻叼", Toast.LENGTH_SHORT).show();
    }
    return true;
  }

為了把懸浮窗的view操作抽離出來,自定義了這個布局,主要進行兩部分功能,懸浮窗的移動和點擊處理,重點是通過mWindowManager.updateViewLayout(this, mWmParams)來進行懸浮窗的位置移動,我這個Demo里面只是簡單的通過時間來判斷點擊事件,有必要的話點擊事件需要添加特定View范圍判斷來響應(yīng)點擊。

// 如果移動量大于3才移動
if (Math.abs(mTouchStartX - mMoveStartX) > 3 && Math.abs(mTouchStartY - mMoveStartY) > 3)

這個判斷是為了避免點擊懸浮窗不在重心位置會出現(xiàn)移動的現(xiàn)象。

FloatMonkService.java

/**
 * 懸浮窗在服務(wù)中創(chuàng)建,通過暴露接口FloatCallBack與Activity進行交互
 */
public class FloatMonkService extends Service implements FloatCallBack {
  /**
   * home鍵監(jiān)聽
   */
  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);
    }
  }

  /////////////////////////////////////////////////////////實現(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ù)開啟的時候通過FloatWindowManager.createFloatWindow(this)來創(chuàng)建懸浮窗,實現(xiàn)FloatCallBack 實現(xiàn)需要交互的接口。下面看一下創(chuàng)建懸浮窗的真正操作是怎樣的。

FloatWindowManager.java

/**
   * 創(chuàng)建一個小懸浮窗。初始位置為屏幕的右下角位置。
   */
  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之后的用戶不必再去手動開啟懸浮窗權(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è)置浮動窗口不可聚焦(實現(xiàn)操作除浮動窗口外的其他可見窗口的操作)
    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;
    //以屏幕左上角為原點,設(shè)置x、y初始值,相對于gravity
    wmParams.x = screenWidth;
    wmParams.y = screenHeight;

    //設(shè)置懸浮窗口長寬數(shù)據(jù)
    wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
    wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
    mFloatLayout.setParams(wmParams);
    windowManager.addView(mFloatLayout, wmParams);
    mHasShown = true;
    //是否展示小紅點展示
    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;
  }

核心代碼其實就是mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE),其中的context不能是Activity的,一開始就說了,Activity會返回它專享的WindowManager,而Activity的窗口級別是屬于應(yīng)用層的。進行一些初始化操作之后 windowManager.addView(mFloatLayout, wmParams)把布局添加進去就ok了。

 if (Build.VERSION.SDK_INT >= 24) { /*android7.0不能用TYPE_TOAST*/
      wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
    } else { /*以下代碼塊使得android6.0之后的用戶不必再去手動開啟懸浮窗權(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;
      }
    }

說一下這段代碼的意義,當(dāng)WindowManager.LayoutParams.type設(shè)置為WindowManager.LayoutParams.TYPE_TOAST的時候,是可以跳過權(quán)限申請的,但是為毛又單獨適配各個機型呢,因為我們有小米Android系統(tǒng),魅族Android系統(tǒng),還有華為等等Android系統(tǒng),特別是產(chǎn)品經(jīng)理的魅族,一些特殊機型上是沒有效果的,所以為了更保險,得再加一份權(quán)限申請,還有一點得提一下,那就是7.0上WindowManager.LayoutParams.TYPE_TOAST,懸浮窗只能持續(xù)一秒的時間,所以7.0不設(shè)這個type,谷歌爸爸最叼,7.0以上老老實實申請權(quán)限。

FloatActionController.java

/**
 * Author:xishuang
 * Date:2017.08.01
 * Des:與懸浮窗交互的控制類,真正的實現(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);
  }

  /**
   * 注冊監(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();
  }
}

這就是暴露出來的接口,按需添加,效果大概是這樣的。

大概效果如下:

Android應(yīng)用內(nèi)懸浮窗的實現(xiàn)方案示例

Demo:代碼地址感興趣可以看看完整的。

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI