溫馨提示×

溫馨提示×

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

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

淺談Android應(yīng)用內(nèi)懸浮控件實踐方案總結(jié)

發(fā)布時間:2020-10-19 15:01:49 來源:腳本之家 閱讀:254 作者:王英豪 欄目:移動開發(fā)

在工作中遇到一個需求,需要在整個應(yīng)用的上層懸浮顯示控件,目標效果如下圖:

淺談Android應(yīng)用內(nèi)懸浮控件實踐方案總結(jié)

首先想到的是申請懸浮窗權(quán)限,OK~ 打開搜索引擎,映入眼簾的并不是如何申請,而是“Android 懸浮窗權(quán)限各機型各系統(tǒng)適配大全、Android 繞過權(quán)限顯示懸浮窗...”,為什么懸浮窗權(quán)限會有這么多坑呢?懸浮窗可以在桌面顯示,被惡意軟件用來偷偷彈廣告怎么辦?作為一個系統(tǒng)級別的特殊權(quán)限,這是它應(yīng)有的高傲 - -

正確引導(dǎo)用戶打開懸浮窗權(quán)限才是標準做法,若這就是定論的話這篇文章也沒必要寫了,我們繞過懸浮窗權(quán)限直接去顯示,大多數(shù)是為了優(yōu)化用戶體驗,并不是惡意的。有時我們只想在自己的應(yīng)用內(nèi)實現(xiàn)懸浮窗,然而 Andorid 并沒有提供這樣的方法,也只好退而求其此的去使用系統(tǒng)級別的懸浮窗權(quán)限。

OK ,既然可以繞過權(quán)限申請,再重新定義一下需求:

盡量繞過申請權(quán)限,實現(xiàn)在 app 指定界面顯示懸浮控件,控件的位置不需要改變

怎么繞過懸浮窗權(quán)限呢?網(wǎng)上大多數(shù)通過 WindowManager 添加一個 TYPE_TOAST 類型的控件,如下:

  WindowManager windowManager = (WindowManager) 
      applicationContext.getSystemService(Context.WINDOW_SERVICE);
  WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
  layoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
  windowManager.addView(view, layoutParams);

而系統(tǒng)在添加 TYPE_TOAST 類型控件時默認不需要權(quán)限,從而可以繞過懸浮窗權(quán)限。但是這種做法并不適配所有機型,比如我親測過的小米(MIUI8) 和 Nexus 7.1.1 機型上就會報錯 Permission Denial ,需要申請權(quán)限,之前這種方式或許可行,但現(xiàn)在肯定不行。

放棄 TYPE_TOAST 方案,不能往窗口里添加視圖,那只能乖乖的申請權(quán)限了嗎?這時你可能想到往所有 Activity 的固定位置添加視圖,模擬“懸浮”效果,比如要實現(xiàn)文章開頭的效果,只需要進入新 Activity 時初始化旋轉(zhuǎn)的角度,讓其在視覺上連續(xù)就行了。

但是要考慮一個問題,在切換 Activity 時舊 Activity 的懸浮控件是要銷毀的,新 Activity 的懸浮控件是要生成的,也就是說在切換 Activity 時這個懸浮控件是會短暫的消失一下,那把 Activity 切換效果設(shè)置為淡入淡出可以嗎,在視覺上是可以實現(xiàn)的,但是嚴格限制了 Activity 的切換效果,不可行。那還有什么方法可以實現(xiàn)切換 Activity 時控件在視覺上連續(xù)嗎?如果你用過共享元素動畫的話,便有答案了。

懸浮控件在哪里添加呢?可以在 BaseActivity 里,也可以為 Application 注冊 Activity 生命周期回調(diào),下面通過后者實現(xiàn),在 Application 中為每個 Activity 添加懸浮控件:

public class BaseApplication extends Application {

  @Override
  public void onCreate() {
    super.onCreate();
    
    registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {

      @Override
      public void onActivityStarted(Activity activity) {
       if(findViewById(R.id.floating_view_id) != null) return;
       View view = LayoutInflater.from(activity).inflate(R.layout.floating_view, null);
       view.setId(R.id.floating_view_id);
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
         view.setTransitionName(activity.getString(R.string.transitionName));
       }
       WindowManager.LayoutParams params = new WindowManager.LayoutParams();
       params.gravity = Gravity.TOP | Gravity.LEFT;
       activity.addContentView(mPopView, mLayoutParams);
}
      
//省略...

切換 Activity 時啟用共享元素動畫:

  Intent intent = new Intent(this, Main2Activity.class);
  View view = findViewById(R.id.floating_view_id);
  if ( view != null) {
    ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
        this,view, getString(R.string.transitionName));
    ContextCompat.startActivity(this, intent, options.toBundle());
  }else{
    startActivity(intent);
  }

這樣就解決了切換 Activity 時懸浮控件短暫消失一下這個問題,然后在添加懸浮控件時,初始化旋轉(zhuǎn)角度就可以實現(xiàn)文章開頭的效果了。但是這種方式存在很大的缺陷,首先就是它不兼容 Andorid 5.0 以下,看看 4.4 那百分之十幾的小伙伴,嗯~ 缺陷很大,其次還有一個致命缺陷,不管把懸浮控件設(shè)為 INVISIBLE 還是透明,只要已經(jīng)添加了此控件,在切換時它都會先顯示一下,這應(yīng)該是共享元素動畫本身的一個 BUG .

OK~ 放棄共享元素方案, 真的繞不過申請權(quán)限了嗎? 再考慮一下 TYPE_TOAST 方案, 為什么它失效了呢? 應(yīng)該是系統(tǒng)對此類型的控件加了限制, 對待 TYPE_TOAST 不再跳過檢查權(quán)限步驟, 而是像 TYPE_PHONE 之類一視同仁, 那為什么我們的 toast 卻可以跳過呢? toast 不就是 TYPE_TOAST 類型的視圖嗎? 不管如何, 反正 toast 是不需要權(quán)限的, 那就嘗試從 toast 入手. OK~ ,現(xiàn)在的關(guān)鍵詞是 自定義 toast .

查看 Toast 類源碼, 有一個方法眼前一亮:

  /**
   * Set the view to show.
   * @see #getView
   */
  public void setView(View view) {
    mNextView = view;
  }

Toast 是可以自定義視圖的, 這為自定義 toast 提供了可能性, 但是顯示時長只能設(shè)置為 LENGTH_SHORT 或 LENGTH_LONG ,我們需要的是無限時長, 沒有方法實現(xiàn), 除非反射之類的怪招了~ 嗯~ 下面奉上通過反射實現(xiàn)無限時長 toast 的完整代碼 :

/**
 * 自定義 toast , 無限時長
 * 可設(shè)置顯示位置 尺寸
 */

class AlwaysShowToast {


  private Toast toast;

  private Object mTN;
  private Method show;
  private Method hide;

  private int mWidth = WindowManager.LayoutParams.WRAP_CONTENT;
  private int mHeight = WindowManager.LayoutParams.WRAP_CONTENT;


  public FixedFloatToast(Context applicationContext) {
    toast = new Toast(applicationContext);
  }


  public void setView(View view, int width, int height) {
    mWidth = width;
    mHeight = height;
    setView(view);
  }


  public void setView(View view) {
    toast.setView(view);
    initTN();
  }


  public void setGravity(int gravity, int xOffset, int yOffset) {
    toast.setGravity(gravity, xOffset, yOffset);
  }


  public void show() {
    try {
      show.invoke(mTN);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }


  public void hide() {
    try {
      hide.invoke(mTN);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }


  /**
   * 利用反射設(shè)置 toast 參數(shù)
   */
  private void initTN() {
    try {
      Field tnField = toast.getClass().getDeclaredField("mTN");
      tnField.setAccessible(true);
      mTN = tnField.get(toast);
      show = mTN.getClass().getMethod("show");
      hide = mTN.getClass().getMethod("hide");

      Field tnParamsField = mTN.getClass().getDeclaredField("mParams");
      tnParamsField.setAccessible(true);
      WindowManager.LayoutParams params = (WindowManager.LayoutParams) tnParamsField.get(mTN);
      params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
          | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
      params.width = mWidth;
      params.height = mHeight;
      Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView");
      tnNextViewField.setAccessible(true);
      tnNextViewField.set(mTN, toast.getView());

    } catch (Exception e) {
      e.printStackTrace();
    }
  }


}

有了這個自定義 toast , 跳過權(quán)限顯示懸浮窗就非常容易了, 理論上可以兼容任意版本,任意機型, 因為這只是一個普通的 toast , 系統(tǒng)沒理由不允許一個 toast 顯示的~ 然而... 親測在 Nexus7.1.1 及以上不顯示 , 在 Android 4.4 以下無法接受觸摸事件, 在小米部分機型上無法改變位置.

OK~ 對比一下這些方案 :

方案1: 申請權(quán)限

   優(yōu)點:實現(xiàn)簡單,只要正確引導(dǎo)用戶打開權(quán)限即可
   缺點:部分機型默認禁用; 需權(quán)限不友好

方案2: 每個界面添加,共享元素過渡

   優(yōu)點:不需權(quán)限
   缺點:較復(fù)雜,只適用于5.0以上,且懸浮控件不可隱藏(共享元素會閃顯控件)

方案3: TYPE_TOAST

   優(yōu)點:實現(xiàn)簡單
   缺點:小米(MIUI8)、7.1.1需要權(quán)限,4.4以下無法接受點擊事件

方案4:自定義 toast

  優(yōu)點:大部分機型不需權(quán)限,實現(xiàn)簡單
  缺點:Nexus7.1.1及以上不顯示,4.4以下無法接受點擊事件,小米(MIUI8)及部分機型不可改變位置

結(jié)合我的需求, 我的懸浮控件并不需要改變位置, 所以最終選擇方案為:

最終方案 : 7.0 以下采用自定義 toast, 7.1 及以上引導(dǎo)用戶申請權(quán)限

如果你的需求也適合此方案的話, 告訴你個好消息, 我已經(jīng)將此方案封裝為可直接調(diào)用的庫 : FixedFloatWindow , 即 fixed (位置固定的) float(懸浮) Window (窗), 可以很方便的使用 :

  FixedFloatWindow fixedFloatWindow = new FixedFloatWindow(getApplicationContext());
  fixedFloatWindow.setView(view);
  fixedFloatWindow.setGravity(Gravity.RIGHT | Gravity.TOP, 100, 150);
  fixedFloatWindow.show();
//  fixedFloatWindow.hide();

最后還有一個問題要解決, 我們要實現(xiàn)的是應(yīng)用內(nèi)懸浮控件 , 此方案應(yīng)用退到后臺后仍然可以在桌面顯示 , 怎么控制呢? 我們可以記錄當(dāng)前 start 的 Activity 數(shù)量, 每當(dāng)有 Activity stop 時, 便將此數(shù)量減 1 , 當(dāng)此數(shù)量為 0 時表示應(yīng)用退到后臺 , 這時隱藏懸浮窗即可 , 類似于這樣:

  @Override
  public void onActivityStarted(Activity activity) {
    mActivityNum++;
    if (isNeedShow(activity)) {
      show();
    }else{
      hide();
    }
  }

  @Override
  public void onActivityStopped(Activity activity) {
    mActivityNum--;
    if (mActivityNum == 0) {
      hide();
    }
  }

關(guān)于文章開頭的實現(xiàn)效果就是用的這種方法, 將懸浮窗控制在應(yīng)用內(nèi)顯示, 效果完整代碼見 FixedFloatWindow 庫 sample 示例 .

FixedFloatWindow 庫地址: https://github.com/yhaolpz/FixedFloatWindow

以上就是本文的全部內(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