Android 應用內懸浮控制項實踐總結

来源:http://www.cnblogs.com/haifengliang/archive/2017/11/28/7908219.html
-Advertisement-
Play Games

在工作中遇到一個需求,需要在整個應用的上層懸浮顯示控制項,目標效果如下圖: 首先想到的是申請懸浮窗許可權,OK~ 打開搜索引擎,映入眼帘的並不是如何申請,而是“Android 懸浮窗許可權各機型各系統適配大全、Android 繞過許可權顯示懸浮窗…”,為什麼懸浮窗許可權會有這麼多坑呢?懸浮窗可以在桌面顯示,被 ...


在工作中遇到一個需求,需要在整個應用的上層懸浮顯示控制項,目標效果如下圖:

這裡寫圖片描述

首先想到的是申請懸浮窗許可權,OK~ 打開搜索引擎,映入眼帘的並不是如何申請,而是“Android 懸浮窗許可權各機型各系統適配大全、Android 繞過許可權顯示懸浮窗…”,為什麼懸浮窗許可權會有這麼多坑呢?懸浮窗可以在桌面顯示,被惡意軟體用來偷偷彈廣告怎麼辦?作為一個系統級別的特殊許可權,這是它應有的高傲 - -

正確引導用戶打開懸浮窗許可權才是標準做法,若這就是定論的話這篇文章也沒必要寫了,我們繞過懸浮窗許可權直接去顯示,大多數是為了優化用戶體驗,並不是惡意的。有時我們只想在自己的應用內實現懸浮窗,然而 Andorid 並沒有提供這樣的方法,也只好退而求其此的去使用系統級別的懸浮窗許可權。

OK ,既然可以繞過許可權申請,再重新定義一下需求:

 儘量繞過申請許可權,實現在 app 指定界面顯示懸浮控制項,控制項的位置不需要改變

怎麼繞過懸浮窗許可權呢?網上大多數通過 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);

而系統在添加 TYPE_TOAST 類型控制項時預設不需要許可權,從而可以繞過懸浮窗許可權。但是這種做法並不適配所有機型,比如我親測過的小米(MIUI8) 和 Nexus 7.1.1 機型上就會報錯 Permission Denial ,需要申請許可權,之前這種方式或許可行,但現在肯定不行。

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

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

懸浮控制項在哪裡添加呢?可以在 BaseActivity 里,也可以為 Application 註冊 Activity 生命周期回調,下麵通過後者實現,在 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 時懸浮控制項短暫消失一下這個問題,然後在添加懸浮控制項時,初始化旋轉角度就可以實現文章開頭的效果了。但是這種方式存在很大的缺陷,首先就是它不相容 Andorid 5.0 以下,看看 4.4 那百分之十幾的小伙伴,嗯~ 缺陷很大,其次還有一個致命缺陷,不管把懸浮控制項設為 INVISIBLE 還是透明,只要已經添加了此控制項,在切換時它都會先顯示一下,這應該是共用元素動畫本身的一個 BUG .

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

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

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

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


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

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();
        }
    }


    /**
     * 利用反射設置 toast 參數
     */
    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 , 跳過許可權顯示懸浮窗就非常容易了, 理論上可以相容任意版本,任意機型, 因為這隻是一個普通的 toast , 系統沒理由不允許一個 toast 顯示的~ 然而… 親測在 Nexus7.1.1 及以上不顯示 , 在 Android 4.4 以下無法接受觸摸事件, 在小米部分機型上無法改變位置.

OK~ 對比一下這些方案 :

方案1: 申請許可權

   優點:實現簡單,只要正確引導用戶打開許可權即可
   缺點:部分機型預設禁用; 需許可權不友好

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

   優點:不需許可權
   缺點:較複雜,只適用於5.0以上,且懸浮控制項不可隱藏(共用元素會閃顯控制項)

方案3: TYPE_TOAST

   優點:實現簡單
   缺點:小米(MIUI8)、7.1.1需要許可權,4.4以下無法接受點擊事件

方案4:自定義 toast

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

結合我的需求, 我的懸浮控制項並不需要改變位置, 所以最終選擇方案為:

最終方案 : 7.0 以下採用自定義 toast, 7.1 及以上引導用戶申請許可權

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

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

最後還有一個問題要解決, 我們要實現的是應用內懸浮控制項 , 此方案應用退到後臺後仍然可以在桌面顯示 , 怎麼控制呢? 我們可以記錄當前 start 的 Activity 數量, 每當有 Activity stop 時, 便將此數量減 1 , 當此數量為 0 時表示應用退到後臺 , 這時隱藏懸浮窗即可 , 類似於這樣:

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

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

源碼免費下載地址:http://www.jinhusns.com/Products/Download/

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. 首先要安裝最新版本的 nodejs 註意:請先在終端/控制台視窗中運行命令 node -v 和 npm -v, 來驗證一下你正在運行 node 6.9.x 和 npm 3.x.x 以上的版本。 更老的版本可能會出現錯誤,更新的版本則沒 問題。 2. 全局安裝Angular CLI 腳手架工具 ...
  • 作者網站: 設置全屏和退出全屏 監聽全屏事件 ...
  • 1.head 1.1.meta標簽 1.2.link標簽 2.body 背景圖片設置 設置背景色,文字顏色 bgcolor:背景色 text:非鏈接文字 link:可鏈接文字 alink:正被點擊的可鏈接文字 vlink:已經點過的可鏈接文字 顏色值,採用十六進位表示,或者英文字母。設置時十六進位借 ...
  • 對於沒參加過互聯網企業招聘,或是沒有參加過大型互聯網企業招聘的人來說,能以這些公司的面試題做為鍛煉,無疑是一種非常好的學習和進步的途徑。下麵是一道騰訊的前端面試題(JS解答),題目本身在現實中意義不大,主要是考察應試者對js及演算法的理解程度,本文給出了三種答案,期待有更大的俠來一試身手,做出更好的解 ...
  • 抽獎代碼里要註意一個地方,就是轉動角度:在電腦語言里,逆時針的轉動才算是正方向,而順時針為負方向。 總結步驟:1.找好圖片素材,當然也可以自己設計一個。(圓盤和指針) 2.先用html將素材寫至頁面當中。 3.設置好樣式,呈現好看的頁面效果。 4.最重要的部分就是在js這塊的實現部分: . (1) ...
  • PS:class的調用,其實是可以疊加的,當然了這要求樣式不同的情況下,如果樣式相同,則後一個樣式會覆蓋前一個樣式。 1.舉例如下: 所以最後‘測試關於class的調用’幾個字的樣式是:font-size:50px; color:green; 2.這樣的添加類方式很繁瑣,每次添加一個新的,我還要帶上 ...
  • 前言 輪播圖已經是一個很常見的東西,尤其是在各大App的首頁頂部欄,經常會輪番顯示不同的圖片。 一提到輪播圖如何實現時,很多人的第一反應就是使用Javascript的定時器,當然這種方法是可以實現的。不過就是有些繁瑣,今天這篇文章我們來看看如何不用Javascript,而使用純CSS代碼去實現輪播圖 ...
  • 以下代碼可以實現在不展開父節點的情況下,勾選父節點後,可以勾選上其下的所有節點,但是如果子節點以及孫節點較多的話,會出現錯誤,需要將延時的時間設置長一點,即 setTimeout(function(){},1000),後面的數字表示延時的毫秒數,並且該種方法的用戶體驗不是很好。 <link rel= ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...