在工作中遇到一個需求,需要在整個應(yīng)用的上層懸浮顯示控件,目標(biāo)效果如下圖:
公司主營業(yè)務(wù):成都網(wǎng)站建設(shè)、做網(wǎng)站、移動網(wǎng)站開發(fā)等業(yè)務(wù)。幫助企業(yè)客戶真正實(shí)現(xiàn)互聯(lián)網(wǎng)宣傳,提高企業(yè)的競爭能力。創(chuàng)新互聯(lián)是一支青春激揚(yáng)、勤奮敬業(yè)、活力青春激揚(yáng)、勤奮敬業(yè)、活力澎湃、和諧高效的團(tuán)隊(duì)。公司秉承以“開放、自由、嚴(yán)謹(jǐn)、自律”為核心的企業(yè)文化,感謝他們對我們的高要求,感謝他們從不同領(lǐng)域給我們帶來的挑戰(zhàn),讓我們激情的團(tuán)隊(duì)有機(jī)會用頭腦與智慧不斷的給客戶帶來驚喜。創(chuàng)新互聯(lián)推出宿州免費(fèi)做網(wǎng)站回饋大家。
首先想到的是申請懸浮窗權(quán)限,OK~ 打開搜索引擎,映入眼簾的并不是如何申請,而是“Android 懸浮窗權(quán)限各機(jī)型各系統(tǒng)適配大全、Android 繞過權(quán)限顯示懸浮窗...”,為什么懸浮窗權(quán)限會有這么多坑呢?懸浮窗可以在桌面顯示,被惡意軟件用來偷偷彈廣告怎么辦?作為一個系統(tǒng)級別的特殊權(quán)限,這是它應(yīng)有的高傲 - -
正確引導(dǎo)用戶打開懸浮窗權(quán)限才是標(biāo)準(zhǔn)做法,若這就是定論的話這篇文章也沒必要寫了,我們繞過懸浮窗權(quán)限直接去顯示,大多數(shù)是為了優(yōu)化用戶體驗(yàn),并不是惡意的。有時我們只想在自己的應(yīng)用內(nèi)實(shí)現(xiàn)懸浮窗,然而 Andorid 并沒有提供這樣的方法,也只好退而求其此的去使用系統(tǒng)級別的懸浮窗權(quán)限。
OK ,既然可以繞過權(quán)限申請,再重新定義一下需求:
盡量繞過申請權(quán)限,實(shí)現(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 類型控件時默認(rèn)不需要權(quán)限,從而可以繞過懸浮窗權(quán)限。但是這種做法并不適配所有機(jī)型,比如我親測過的小米(MIUI8) 和 Nexus 7.1.1 機(jī)型上就會報錯 Permission Denial ,需要申請權(quán)限,之前這種方式或許可行,但現(xiàn)在肯定不行。
放棄 TYPE_TOAST 方案,不能往窗口里添加視圖,那只能乖乖的申請權(quán)限了嗎?這時你可能想到往所有 Activity 的固定位置添加視圖,模擬“懸浮”效果,比如要實(shí)現(xiàn)文章開頭的效果,只需要進(jìn)入新 Activity 時初始化旋轉(zhuǎn)的角度,讓其在視覺上連續(xù)就行了。
但是要考慮一個問題,在切換 Activity 時舊 Activity 的懸浮控件是要銷毀的,新 Activity 的懸浮控件是要生成的,也就是說在切換 Activity 時這個懸浮控件是會短暫的消失一下,那把 Activity 切換效果設(shè)置為淡入淡出可以嗎,在視覺上是可以實(shí)現(xiàn)的,但是嚴(yán)格限制了 Activity 的切換效果,不可行。那還有什么方法可以實(shí)現(xiàn)切換 Activity 時控件在視覺上連續(xù)嗎?如果你用過共享元素動畫的話,便有答案了。
懸浮控件在哪里添加呢?可以在 BaseActivity 里,也可以為 Application 注冊 Activity 生命周期回調(diào),下面通過后者實(shí)現(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)角度就可以實(shí)現(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 ,我們需要的是無限時長, 沒有方法實(shí)現(xiàn), 除非反射之類的怪招了~ 嗯~ 下面奉上通過反射實(shí)現(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)限顯示懸浮窗就非常容易了, 理論上可以兼容任意版本,任意機(jī)型, 因?yàn)檫@只是一個普通的 toast , 系統(tǒng)沒理由不允許一個 toast 顯示的~ 然而... 親測在 Nexus7.1.1 及以上不顯示 , 在 Android 4.4 以下無法接受觸摸事件, 在小米部分機(jī)型上無法改變位置.
OK~ 對比一下這些方案 :
方案1: 申請權(quán)限
優(yōu)點(diǎn):實(shí)現(xiàn)簡單,只要正確引導(dǎo)用戶打開權(quán)限即可
缺點(diǎn):部分機(jī)型默認(rèn)禁用; 需權(quán)限不友好
方案2: 每個界面添加,共享元素過渡
優(yōu)點(diǎn):不需權(quán)限
缺點(diǎn):較復(fù)雜,只適用于5.0以上,且懸浮控件不可隱藏(共享元素會閃顯控件)
方案3: TYPE_TOAST
優(yōu)點(diǎn):實(shí)現(xiàn)簡單
缺點(diǎn):小米(MIUI8)、7.1.1需要權(quán)限,4.4以下無法接受點(diǎn)擊事件
方案4:自定義 toast
優(yōu)點(diǎn):大部分機(jī)型不需權(quán)限,實(shí)現(xiàn)簡單
缺點(diǎn):Nexus7.1.1及以上不顯示,4.4以下無法接受點(diǎn)擊事件,小米(MIUI8)及部分機(jī)型不可改變位置
結(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();
最后還有一個問題要解決, 我們要實(shí)現(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)于文章開頭的實(shí)現(xiàn)效果就是用的這種方法, 將懸浮窗控制在應(yīng)用內(nèi)顯示, 效果完整代碼見 FixedFloatWindow 庫 sample 示例 .
FixedFloatWindow 庫地址: https://github.com/yhaolpz/FixedFloatWindow
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。