不需要权限的悬浮窗

需求开发中有时会遇到需要在别的应用上显示自己的内容的情况,比如展示一个消息通知按钮,允许用户通过这个按钮直接进入我们的应用。然而展示悬浮窗需要申请 SYSTEM_ALERT_WINDOW 权限。

产品角度出发肯定是不向用户展示这个权限更好,那么以下是具体的实现方法。首先介绍的是普遍方法,然后针对 MIUI8 这个特殊的系统做单独的处理。

需要补充的是对于4.4以下的系统,不添加权限的情况下最多只能展示而不能响应点击。所以这部分需要区分处理。

Github链接:https://github.com/ZhengPhoenix/CustomFloatWindow

动态添加悬浮窗

这里介绍在service中添加悬浮窗到WindowManager的过程

  1. 普通悬浮窗添加代码流程如下

/**
 * 创建悬浮窗
 */
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();

    // 设置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;
    }


    /*
     * 如果设置为params.type = WindowManager.LayoutParams.TYPE_PHONE; 那么优先级会降低一些,
     * 即拉下通知栏不可见
     */
    sParams.format = PixelFormat.RGBA_8888; // 设置图片格式,效果为背景透明

    // 设置Window flag
    sParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
            | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

    // 设置悬浮窗的长得宽
    sParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
    sParams.height = WindowManager.LayoutParams.WRAP_CONTENT;

    //计算高度
    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;
}

这个流程先inflate了一个自定义的view R.layout.floating_entrance ,然后获取WindowManagerService,再添加该view到WM。这个view在1000ms后会自动移除自己,同时它可以响应用户的点击事件。

为了对4.4以下的系统做兼容,添加了这部分代码,同时也在AndroidManifest里申请了相应的权限

//AndroidManifest


// 设置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以下的系统如果设置为toast类型,则不能响应点击事件。必须设置为 TYPE_SYSTEM_ALERT 才可以。

接下来需要设置悬浮窗的属性,让它能够响应事件

//设置Window flag

sParams.flags= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL

| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;```

完成到这里之后,就可以根据自己需要设置窗口位置,动画等效果了。

最后添加view到wm就可以

wm.addView(mFloatView,sParams);




2.MIUI8悬浮窗添加流程

上面这种方式在MIUI8中是无效的。原因在于MIUI8对悬浮窗的权限做了限制,除非打开悬浮窗权限SYSTEM_ALERT_WINDOW,否则无法通过这种方式展示悬浮窗。

然而经过测试发现普通的Toast在没有权限的情况下是可以显示的,当然这毫不意外。那么这就有空子可钻了,我们可以自定义一个view给Toast,然后再将这个toast显示出来。这里需要解决几个问题,首先是Toast的触摸响应,其次是它的某些方法是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;

        /**设置动画*/
        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();
    }
}

public void setListener(View.OnClickListener listener) {
    mView.setOnClickListener(listener);
}

}


这个类封装了所有需要的方法和对象,使用时只需要简单的实例化和调用show和dismiss方法就可以。

需要关注的是 initTN() 方法,这里面除了获取了show和hide,重点还获取了 params 对象,并向它设置了可触摸的属性。

```params.flags= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL

| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;```

对的就是这么扯竟然是 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE ...

下面是调用这个封装好的 ExToast 的 方法,

/**

  • 创建 MIUI8 悬浮窗
    */
    private void createFloatViewForMiUi() {
    wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
    //计算高度
    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的情况做对应处理就行,

比如调用  wm.removeView 和 mFloatViewMIUI.hide。

关于不需要权限展示悬浮窗的思路和代码就是这样了,但是MIUI一直在更新中,不排除有新的版本堵上了这个漏洞,所以如果有使用不了的情况也是很正常的╮(╯▽╰)╭

你可能感兴趣的:(不需要权限的悬浮窗)