需求開發(fā)中有時(shí)會(huì)遇到需要在別的應(yīng)用上顯示自己的內(nèi)容的情況慰枕,比如展示一個(gè)消息通知按鈕呛踊,允許用戶通過這個(gè)按鈕直接進(jìn)入我們的應(yīng)用砾淌。然而展示懸浮窗需要申請 SYSTEM_ALERT_WINDOW 權(quán)限。
產(chǎn)品角度出發(fā)肯定是不向用戶展示這個(gè)權(quán)限更好谭网,那么以下是具體的實(shí)現(xiàn)方法汪厨。首先介紹的是普遍方法,然后針對 MIUI8 這個(gè)特殊的系統(tǒng)做單獨(dú)的處理愉择。
需要補(bǔ)充的是對于4.4以下的系統(tǒng)劫乱,不添加權(quán)限的情況下最多只能展示而不能響應(yīng)點(diǎn)擊。所以這部分需要區(qū)分處理锥涕。
Github鏈接:https://github.com/ZhengPhoenix/CustomFloatWindow
動(dòng)態(tài)添加懸浮窗
這里介紹在service中添加懸浮窗到WindowManager的過程
- 普通懸浮窗添加代碼流程如下
/**
* 創(chuàng)建懸浮窗
*/
private void createFloatView() {
LayoutInflater layoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mFloatView = layoutInflater.inflate(R.layout.floating_entrance, null);
wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
sParams = new WindowManager.LayoutParams();
// 設(shè)置window type
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
sParams.type = WindowManager.LayoutParams.TYPE_TOAST;
} else {
sParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
/*
* 如果設(shè)置為params.type = WindowManager.LayoutParams.TYPE_PHONE; 那么優(yōu)先級(jí)會(huì)降低一些,
* 即拉下通知欄不可見
*/
sParams.format = PixelFormat.RGBA_8888; // 設(shè)置圖片格式衷戈,效果為背景透明
// 設(shè)置Window flag
sParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
// 設(shè)置懸浮窗的長得寬
sParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
sParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
//計(jì)算高度
DisplayMetrics metric = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metric);
int yPosition = Math.round(wm.getDefaultDisplay().getHeight() / 3);
sParams.gravity = Gravity.RIGHT | Gravity.TOP;
sParams.x = 0;
sParams.y = yPosition;
AnimationDrawable drawable = (AnimationDrawable) ((ImageView) mFloatView.findViewById(R.id.floating_entrance_anim)).getDrawable();
drawable.start();
wm.addView(mFloatView, sParams);
mFloatView.postDelayed(new Runnable() {
@Override
public void run() {
try {
wm.removeView(mFloatView);
} catch (Exception e) {
//incase floating window has already dismissed
}
FloatingWindowService.this.stopSelf();
}
}, DURATION * 1000);
isAdded = true;
}
這個(gè)流程先inflate了一個(gè)自定義的view R.layout.floating_entrance ,然后獲取WindowManagerService层坠,再添加該view到WM殖妇。這個(gè)view在1000ms后會(huì)自動(dòng)移除自己,同時(shí)它可以響應(yīng)用戶的點(diǎn)擊事件窿春。
為了對4.4以下的系統(tǒng)做兼容拉一,添加了這部分代碼采盒,同時(shí)也在AndroidManifest里申請了相應(yīng)的權(quán)限
//AndroidManifest
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
// 設(shè)置window type
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
sParams.type = WindowManager.LayoutParams.TYPE_TOAST;
} else {
sParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
這里的問題是,4.4以下的系統(tǒng)如果設(shè)置為toast類型蔚润,則不能響應(yīng)點(diǎn)擊事件磅氨。必須設(shè)置為 TYPE_SYSTEM_ALERT 才可以。
接下來需要設(shè)置懸浮窗的屬性嫡纠,讓它能夠響應(yīng)事件
//設(shè)置Window flag
sParams.flags= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;```
完成到這里之后烦租,就可以根據(jù)自己需要設(shè)置窗口位置,動(dòng)畫等效果了除盏。
最后添加view到wm就可以
wm.addView(mFloatView,sParams);
2.MIUI8懸浮窗添加流程
上面這種方式在MIUI8中是無效的叉橱。原因在于MIUI8對懸浮窗的權(quán)限做了限制,除非打開懸浮窗權(quán)限SYSTEM_ALERT_WINDOW者蠕,否則無法通過這種方式展示懸浮窗窃祝。
然而經(jīng)過測試發(fā)現(xiàn)普通的Toast在沒有權(quán)限的情況下是可以顯示的,當(dāng)然這毫不意外踱侣。那么這就有空子可鉆了粪小,我們可以自定義一個(gè)view給Toast,然后再將這個(gè)toast顯示出來。這里需要解決幾個(gè)問題抡句,首先是Toast的觸摸響應(yīng)探膊,其次是它的某些方法是private的,包括需要修改的LayoutParameters對象也是private待榔,這部分需要用反射來獲得逞壁。
/**
Created by zhenghui on 2016/8/24.
This is a class created to make sure custom floating view
-
could be shown in MIUI8
*/
public class ExToast {
private static final String TAG = "ExToast";public static final int LENGTH_ALWAYS = 0;
public static final int LENGTH_SHORT = 2;
public static final int LENGTH_LONG = 4;private Toast toast;
private Context mContext;
private int mDuration = LENGTH_SHORT;
private int animations = -1;
private boolean isShow = false;private Object mTN;
private Method show;
private Method hide;
private WindowManager mWM;
private WindowManager.LayoutParams params;
private View mView;private Handler handler = new Handler();
public ExToast(Context context){
this.mContext = context;
if (toast == null) {
toast = new Toast(mContext);
}
LayoutInflater inflate = (LayoutInflater)
mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mView = inflate.inflate(R.layout.floating_entrance, null);
}private Runnable hideRunnable = new Runnable() {
@Override
public void run() {
hide();
}
};/**
-
Show the view for the specified duration.
*/
public void show(){
if (isShow) return;
toast.setView(mView);
initTN();
try {
show.invoke(mTN);
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
isShow = true;if (mDuration > LENGTH_ALWAYS) {
handler.postDelayed(hideRunnable, mDuration * 1000);
}
}
/**
Close the view if it's showing, or don't show it if it isn't showing yet.
You do not normally have to call this. Normally view will disappear on its own
-
after the appropriate duration.
*/
public void hide(){
if(!isShow) return;
try {
hide.invoke(mTN);
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}isShow = false;
}
public void setView(View view) {
toast.setView(view);
} -
......
public static ExToast makeText(Context context, CharSequence text, int duration) {
Toast toast = Toast.makeText(context,text,Toast.LENGTH_SHORT);
ExToast exToast = new ExToast(context);
exToast.toast = toast;
exToast.mDuration = duration;
return exToast;
}
public static ExToast makeText(Context context, int resId, int duration)
throws Resources.NotFoundException {
return makeText(context, context.getResources().getText(resId), duration);
}
public void setText(int resId) {
setText(mContext.getText(resId));
}
public void setText(CharSequence s) {
toast.setText(s);
}
public int getAnimations() {
return animations;
}
public void setAnimations(int animations) {
this.animations = animations;
}
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);
params = (WindowManager.LayoutParams) tnParamsField.get(mTN);
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
/**設(shè)置動(dòng)畫*/
if (animations != -1) {
params.windowAnimations = animations;
}
/**調(diào)用tn.show()之前一定要先設(shè)置mNextView*/
Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView");
tnNextViewField.setAccessible(true);
tnNextViewField.set(mTN, toast.getView());
mWM = (WindowManager)mContext.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
} catch (Exception e) {
e.printStackTrace();
}
}
public void setListener(View.OnClickListener listener) {
mView.setOnClickListener(listener);
}
}
這個(gè)類封裝了所有需要的方法和對象,使用時(shí)只需要簡單的實(shí)例化和調(diào)用show和dismiss方法就可以锐锣。
需要關(guān)注的是 initTN() 方法腌闯,這里面除了獲取了show和hide,重點(diǎn)還獲取了 params 對象刺下,并向它設(shè)置了可觸摸的屬性绑嘹。
```params.flags= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;```
對的就是這么扯竟然是 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE ...
下面是調(diào)用這個(gè)封裝好的 ExToast 的 方法,
/**
- 創(chuàng)建 MIUI8 懸浮窗
*/
private void createFloatViewForMiUi() {
wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
//計(jì)算高度
DisplayMetrics metric = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(metric);
int yPosition = Math.round(wm.getDefaultDisplay().getHeight() / 3);
mFloatViewMIUI = new ExToast(getApplicationContext());
mFloatViewMIUI.setDuration(DURATION);
mFloatViewMIUI.setAnimations(R.style.float_search);
mFloatViewMIUI.setGravity(Gravity.RIGHT | Gravity.TOP, 0, yPosition);
mFloatViewMIUI.show();
isAdded = true;
}
到這里就可以把懸浮窗顯示出來了橘茉。最后想要隱藏或者去除的話針對一般的和MIUI8的情況做對應(yīng)處理就行工腋,
比如調(diào)用 wm.removeView 和 mFloatViewMIUI.hide。
關(guān)于不需要權(quán)限展示懸浮窗的思路和代碼就是這樣了畅卓,但是MIUI一直在更新中擅腰,不排除有新的版本堵上了這個(gè)漏洞,所以如果有使用不了的情況也是很正常的╮(╯▽╰)╭