在工做中遇到一個需求,須要在整個應用的上層懸浮顯示控件,目標效果以下圖:java
首先想到的是申請懸浮窗權限,OK~ 打開搜索引擎,映入眼簾的並非如何申請,而是「Android 懸浮窗權限各機型各系統適配大全、Android 繞過權限顯示懸浮窗…」,爲何懸浮窗權限會有這麼多坑呢?懸浮窗能夠在桌面顯示,被惡意軟件用來偷偷彈廣告怎麼辦?做爲一個系統級別的特殊權限,這是它應有的高傲 - -app
正確引導用戶打開懸浮窗權限纔是標準作法,若這就是定論的話這篇文章也不必寫了,咱們繞過懸浮窗權限直接去顯示,大多數是爲了優化用戶體驗,並非惡意的。有時咱們只想在本身的應用內實現懸浮窗,然而 Andorid 並無提供這樣的方法,也只好退而求其此的去使用系統級別的懸浮窗權限。ide
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 ,須要申請權限,以前這種方式或許可行,但如今確定不行。ui
放棄 TYPE_TOAST 方案,不能往窗口裏添加視圖,那隻能乖乖的申請權限了嗎?這時你可能想到往全部 Activity 的固定位置添加視圖,模擬「懸浮」效果,好比要實現文章開頭的效果,只須要進入新 Activity 時初始化旋轉的角度,讓其在視覺上連續就好了。this
可是要考慮一個問題,在切換 Activity 時舊 Activity 的懸浮控件是要銷燬的,新 Activity 的懸浮控件是要生成的,也就是說在切換 Activity 時這個懸浮控件是會短暫的消失一下,那把 Activity 切換效果設置爲淡入淡出能夠嗎,在視覺上是能夠實現的,可是嚴格限制了 Activity 的切換效果,不可行。那還有什麼方法能夠實現切換 Activity 時控件在視覺上連續嗎?若是你用過共享元素動畫的話,便有答案了。搜索引擎
懸浮控件在哪裏添加呢?能夠在 BaseActivity 裏,也能夠爲 Application 註冊 Activity 生命週期回調,下面經過後者實現,在 Application 中爲每一個 Activity 添加懸浮控件:spa
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 時啓用共享元素動畫:code
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/