編輯:關於Android編程
在上一篇文章講了Android的Toast拓展,在原生Toast基礎上對顯示時長和顯示動畫做了二次封裝,強化了Toast的部分功能。也分析了對於二次封裝的ExToast設計原理,以及Toast的關鍵點。
之前分析過,Toast其實就是系統懸浮窗的一種,那它跟常用的系統懸浮窗有什麼區別呢?
先看一下常用的Andoird系統懸浮窗寫法:
// 獲取應用的Context
mContext = context.getApplicationContext();
// 獲取WindowManager
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
mView = setUpView(context);
final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
// 類型
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
int flags = WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
params.flags = flags;
params.format = PixelFormat.TRANSLUCENT;
params.width = LayoutParams.MATCH_PARENT;
params.height = LayoutParams.MATCH_PARENT;
params.gravity = Gravity.CENTER;
mWindowManager.addView(mView, params);
再看看在Toast源碼裡面的寫法關鍵代碼:
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
// 類型
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
...
// 獲取應用的context
Context context = mView.getContext().getApplicationContext();
// 獲取WindowManager
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
...
if (mView.getParent() != null) {
mWM.removeView(mView);
}
mWM.addView(mView, mParams);
上面的兩段代碼大致流程都是一樣的:創建WindowManager.LayoutParams做窗口的配置->通過context獲取WindowManager服務->通過WindowManager服務添加懸浮窗View
主要的不同點在於WindowManager.LayoutParams的type。
WindowManager.LayoutParams的type有很多種,包括各種系統對話框,鎖屏窗口,電話窗口等等,但這些窗口基本上都是需要權限的。
而我們平時使用的Toast,並不需要權限就能顯示,那就可以嘗試直接把懸浮窗的類型設成TYPE_TOAST,來定制一個不需要權限的懸浮窗。
下面是demo代碼:
import android.content.Context;
import android.graphics.PixelFormat;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
public class ADToast implements View.OnTouchListener {
Context mContext;
WindowManager.LayoutParams params;
WindowManager mWM;
View mView;
private float mTouchStartX;
private float mTouchStartY;
private float x;
private float y;
public ADToast(Context context){
this.mContext = context;
params = new WindowManager.LayoutParams();
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = R.style.anim_view;
// 懸浮窗類型,整個demo的關鍵點
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.gravity = Gravity.LEFT | Gravity.TOP;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
LayoutInflater inflate = (LayoutInflater)
mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mView = inflate.inflate(R.layout.float_tips_layout, null);
mView.setOnTouchListener(this);
}
public void show(){
TextView tv = (TextView)mView.findViewById(R.id.message);
tv.setText("懸浮窗");
if (mView.getParent() != null) {
mWM.removeView(mView);
}
mWM.addView(mView, params);
}
public void hide(){
if(mView!=null){
mWM.removeView(mView);
}
}
public void setText(String text){
TextView tv = (TextView)mView.findViewById(R.id.message);
tv.setText(text);
}
private void updateViewPosition(){
//更新浮動窗口位置參數
params.x=(int) (x-mTouchStartX);
params.y=(int) (y-mTouchStartY);
mWM.updateViewLayout(mView, params); //刷新顯示
}
@Override
public boolean onTouch(View v, MotionEvent event) {
//獲取相對屏幕的坐標,即以屏幕左上角為原點
x = event.getRawX();
y = event.getRawY();
Log.i("currP", "currX"+x+"====currY"+y);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: //捕獲手指觸摸按下動作
//獲取相對View的坐標,即以此View左上角為原點
mTouchStartX = event.getX();
mTouchStartY = event.getY();
Log.i("startP","startX"+mTouchStartX+"====startY"+mTouchStartY);
break;
case MotionEvent.ACTION_MOVE: //捕獲手指觸摸移動動作
updateViewPosition();
break;
case MotionEvent.ACTION_UP: //捕獲手指觸摸離開動作
updateViewPosition();
break;
}
return true;
}
}
float_tips_layout.xml
使用N5原生6.0系統測試通過,使用一加3測試通過,使用魅族pro5測試通過。只有小米MIUI8,對Toast類型懸浮窗做了權限控制。
實測在MIUI8中,打開懸浮窗權限可以顯示這種Toast類型的懸浮窗。而使用原生Toast類,卻不需要權限就可以顯示,看來小米的系統在framework層對Toast類型的權限做了特殊處理。
但是,只要Toast能顯示,就說明肯定有方法繞過去。最好的方法,就是把小米改動的framework層代碼扒出來,看看原生Toast和自定義Toast類型懸浮窗在權限處理上的區別是什麼,但是有一定的難度,在研究了一天無果後,先使用了第二種更容易實現的方法。
既然原生Toast不需要權限,那我們就在原生Toast的基礎上繼續封裝拓展。上一篇Toast拓展文章已經對Toast的二次封裝解釋的比較詳細了,下面直接上Demo代碼。
import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MiExToast implements View.OnTouchListener {
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 float mTouchStartX;
private float mTouchStartY;
private float x;
private float y;
private Handler handler = new Handler();
public MiExToast(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.float_tips_layout, null);
mView.setOnTouchListener(this);
}
private Runnable hideRunnable = new Runnable() {
@Override
public void run() {
hide();
}
};
/**
* Show the view for the specified duration.
*/
public void show(){
if (isShow) return;
TextView tv = (TextView)mView.findViewById(R.id.message);
tv.setText("懸浮窗");
toast.setView(mView);
initTN();
try {
show.invoke(mTN);
} catch (InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
isShow = true;
//判斷duration,如果大於#LENGTH_ALWAYS 則設置消失時間
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 | IllegalAccessException e) {
e.printStackTrace();
}
isShow = false;
}
public void setView(View view) {
toast.setView(view);
}
public View getView() {
return toast.getView();
}
/**
* Set how long to show the view for.
* @see #LENGTH_SHORT
* @see #LENGTH_LONG
* @see #LENGTH_ALWAYS
*/
public void setDuration(int duration) {
mDuration = duration;
}
public int getDuration() {
return mDuration;
}
public void setMargin(float horizontalMargin, float verticalMargin) {
toast.setMargin(horizontalMargin,verticalMargin);
}
public float getHorizontalMargin() {
return toast.getHorizontalMargin();
}
public float getVerticalMargin() {
return toast.getVerticalMargin();
}
public void setGravity(int gravity, int xOffset, int yOffset) {
toast.setGravity(gravity,xOffset,yOffset);
}
public int getGravity() {
return toast.getGravity();
}
public int getXOffset() {
return toast.getXOffset();
}
public int getYOffset() {
return toast.getYOffset();
}
public static MiExToast makeText(Context context, CharSequence text, int duration) {
Toast toast = Toast.makeText(context,text,Toast.LENGTH_SHORT);
MiExToast exToast = new MiExToast(context);
exToast.toast = toast;
exToast.mDuration = duration;
return exToast;
}
public static MiExToast 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;
/**設置動畫*/
if (animations != -1) {
params.windowAnimations = animations;
}
/**調用tn.show()之前一定要先設置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();
}
setGravity(Gravity.LEFT | Gravity.TOP,0 ,0);
}
private void updateViewPosition(){
//更新浮動窗口位置參數
params.x=(int) (x-mTouchStartX);
params.y=(int) (y-mTouchStartY);
mWM.updateViewLayout(toast.getView(), params); //刷新顯示
}
@Override
public boolean onTouch(View v, MotionEvent event) {
//獲取相對屏幕的坐標,即以屏幕左上角為原點
x = event.getRawX();
y = event.getRawY();
Log.i("currP", "currX"+x+"====currY"+y);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: //捕獲手指觸摸按下動作
//獲取相對View的坐標,即以此View左上角為原點
mTouchStartX = event.getX();
mTouchStartY = event.getY();
Log.i("startP","startX"+mTouchStartX+"====startY"+mTouchStartY);
break;
case MotionEvent.ACTION_MOVE: //捕獲手指觸摸移動動作
updateViewPosition();
break;
case MotionEvent.ACTION_UP: //捕獲手指觸摸離開動作
updateViewPosition();
break;
}
return true;
}
}
example:
MiExToast miToast = new MiExToast(getApplicationContext());
miToast.setDuration(MiExToast.LENGTH_ALWAYS);
miToast.setAnimations(R.style.anim_view);
miToast.show();
上面的Demo類是基於上一篇文章Toast拓展–自定義顯示時間和動畫,進行再次拓展做出來的,它只是一個Demo,並不是工具類,不能直接拿來使用。
下面根據這個Demo,我們來分析它的原理。
下面有三個關鍵點:
1. Toast是可以自定義View的
2. 懸浮窗的觸摸需要修改WindowManager.LayoutParams.flags,設置WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
3. 刷新懸浮窗,只需要獲得WindowManager實例,調用updateViewLayout並傳入View和LayoutParams即可
經過上一篇文章的講解,對於Toast的LayoutParams實例我們可以通過反射獲得,並且給他設置上可觸摸的flag。關注上面代碼的initTN()方法,獲得的LayoutParams實例需要保持引用,因為後面還需要用上。
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;
然後是對二次封裝的Demo類MiExToast裡面的Toast實例設置View。這個應該很容易理解,Toast是可以自定義View的,設置自己的View作為懸浮窗。同時,可以對View添加一些自定義的Touch事件,在這個Demo中用戶可以隨意拖動懸浮窗。
public void init(){
mView = inflate.inflate(R.layout.float_tips_layout, null);
mView.setOnTouchListener(this);
toast.setView(mView);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
//獲取相對屏幕的坐標,即以屏幕左上角為原點
x = event.getRawX();
y = event.getRawY();
...
return true;
}
最後就是對懸浮窗的更新,只需要通過context獲取到WindowManager,即可調用updateViewLayout對懸浮窗進行更新。
private WindowManager.LayoutParams params;
private void updateViewPosition(){
mWM.updateViewLayout(toast.getView(), params); //刷新顯示
}
大致原理就是這樣,借助原生Toast顯示自定義的懸浮窗,越過小米MIUI8對於Toast類型懸浮窗的權限封鎖。
最後上一個小米系統示例圖:
第一步、先爆項目demo照片,代碼不多,不要怕 第二步、應該知道Java反射相關知識如果不知道或者忘記的小伙伴請猛搓這裡,Android插件化開發基礎之Java
蘑菇ROM助手可以對system.img文件進行合並或分割,下面就讓我給大家講講如何操作。一、操作前准備1、下載安裝ROM助手2、准備好刷機包二、打開ROM
寫在前面:作為一個剛半只腳踏入android開發的新手,在使用eclipse開發了兩個自我感覺不甚成熟的商城類app之後,遇到了一些問題,總結為如下:1,代碼復用性。fi
先來看看要實現的效果圖:對於安卓用戶來說,手機應用市場說滿天飛可是一點都不誇張,比如小米,魅族,百度,360,機鋒,應用寶等等,當我們想上線一款新版本APP時,先不說渠道