精华内容
下载资源
问答
  • 解决通知栏权限禁止时,不弹出Toast问题项目里使用Toast时不知道有木有遇到跟我同样困惑的童鞋?1、自定义系统Toast弹入,弹出动画2、某些品牌手机里,如果权限管理上禁止了“通知栏消息”权限时,Toast显示不出来。...

    解决通知栏权限禁止时,不弹出Toast问题

    项目里使用Toast时不知道有木有遇到跟我同样困惑的童鞋?

    1、自定义系统Toast弹入,弹出动画

    2、某些品牌手机里,如果权限管理上禁止了“通知栏消息”权限时,Toast显示不出来。(这里以魅族手机为例,不同厂家手机可能权限名不一样,但大概意思差不多)

    下面是个人对这解决这两个问题的一个总结。

    9bcc2c186fd3

    1、自定义Toast动画

    关于Taost的自定义动画,google官方其实是没有直接暴露更改动画的api。通过查看源码,可以知道系统Toast的动画代码是TN里面实现的,遗憾的是TN被私有了。那么想要直接通过这个方法来更改动画是不行了,只有使用反射去实现了

    Toast mToast = Toast.makeText(context, "", Toast.LENGTH_SHORT);

    try {

    Object mTN = null;

    mTN = getField(mToast, "mTN");

    if (mTN != null) {

    Object mParams = getField(mTN, "mParams");

    if (mParams != null

    && mParams instanceof WindowManager.LayoutParams) {

    WindowManager.LayoutParams params = (WindowManager.LayoutParams) mParams;

    params.windowAnimations = R.style.custom_animation_toast;

    }

    }

    } catch (Exception e) {

    }

    2、权限限制Toast弹出

    Demo代码量有点多,已放到github

    展开全文
  • 拓展toast出现与消失动画简介:我们在Android应用开发中经常会需要在界面上弹出一个对界面操作无影响小提示框来提示用户一些信息,一般都会使用Android原生的ToastToast.makeText(mContext, "消息内容", Toast....

    ExToast

    功能点:

    1.拓展toast显示时间,可以自定义任意时间或一直显示

    2.拓展toast出现与消失动画

    简介:

    我们在Android应用开发中经常会需要在界面上弹出一个对界面操作无影响小提示框来提示用户一些信息,一般都会使用Android原生的Toast类

    Toast.makeText(mContext, "消息内容", Toast.LENGTH_SHORT).show();

    一开始觉得,挺好用的,就有点什么消息都用Toast显示了。

    但是用就了就发现,Toast的默认样式有点丑,显示和消失动画也不符合自己的要求,显示时间也只有SHORT和LONG两种选择,限制太多了。

    于是,在阅读了Toast的源码后对Toast进行了拓展,原生Toast包含了以下方法给用户修改显示内容:

    setView(View):void

    setDuration(int):void

    setMargin(float,float):void

    setGravity(int,int,int):void

    setText(int):void

    setText(CharSequence):void

    分别是直接替换视图、设置显示时长、设置边距属性、设置显示位置、设置显示文字内容。

    基于原生Toast拓展了两个方法:

    setDuration(int):void

    setAnimations(int):void

    设置显示时长方法拓展为可以自定义显示时间,参数单位秒,提供三个默认值:LENGTH_SHORT,LENGTH_LONG,LENGTH_ALWAYS,分别对应原生Toast的LENGTH_SHORT,LENGTH_LONG,以及总是显示。要注意的是总是显示需要在合适的时候自己调用hide()方法隐藏,否则会影响其他窗口看的正常显示。

    ExToast example:

    ExToast exToast = ExToast.makeText(context,"message",ExToast.LENGTH_ALWAYS);

    exToast.setAnimations(R.style.anim_view);

    exToast.show();

    //使用LENGTH_ALWAYS注意在合适的时候调用hide()

    exToast.hide();

    上面的代码可以实现自定义xml窗口动画,以及长时间显示Toast的功能。

    下面看一下R.style.anim_view的内容,窗口动画可以通过@android:windowEnterAnimation和@android:windowExitAnimation定义窗口进场及退场效果

    style.xml

    @anim/anim_in

    @anim/anim_out

    anim_in.xml

    android:fromXDelta="0"

    android:fromYDelta="0"

    android:toXDelta="0"

    android:toYDelta="85"

    android:duration="1"

    />

    android:fromXDelta="0"

    android:fromYDelta="0"

    android:toXDelta="0"

    android:toYDelta="-105"

    android:duration="350"

    android:fillAfter="true"

    android:interpolator="@android:anim/decelerate_interpolator"

    />

    android:fromAlpha="0"

    android:toAlpha="1"

    android:duration="100"

    />

    android:fromXDelta="0"

    android:fromYDelta="0"

    android:toXDelta="0"

    android:toYDelta="20"

    android:duration="80"

    android:fillAfter="true"

    android:startOffset="350"

    />

    anim_out.xml

    android:fromAlpha="1"

    android:toAlpha="0"

    android:duration="800"/>

    具体效果请运行demo

    ExToast原理解析

    使用过Toast都知道Toast只提供了两个长度的时间,分别为LENGTH_SHORT,LENGTH_LONG,它们的时长分别是2秒和大约3秒,在3秒内的Toast,我们都可以通过toast.cancle()取消显示,但如果要显示一个时长大于3秒的Toast时就无能为力了。

    显示时间问题还不是最致命的,最致命的问题,是系统原生的Toast是呈队列显示出来的,必须要等到前一条Toast消失才会显示下一条。

    相信很多同学都遇到过这个问题,比如我做一个按钮,点击的时候显示一个toast,然后做了个小小的压力测试:狂按保存按钮!于是toast队列排了好长一条,一直在显示,等到一两分钟才结束。

    通过阅读Toast源码,可以看到里面的show()方法:

    public void show() {

    if (mNextView == null) {

    throw new RuntimeException("setView must have been called");

    }

    INotificationManager service = getService();

    String pkg = mContext.getPackageName();

    TN tn = mTN;

    tn.mNextView = mNextView;

    try {

    service.enqueueToast(pkg, tn, mDuration);

    } catch (RemoteException e) {

    // Empty

    }

    }

    可以看到Toast的核心显示和隐藏是封装在INotificationManager的enqueueToast方法中,看到enqueue这个词就知道这是一个队列处理的函数,它的参数分别是packageName,tn对象,持续时间。结合Toast的显示效果我们可以猜测这个方法内部实现是队列显示和隐藏每一个传入的Toast。packageName和持续时间我们都很清楚是什么,剩下的重点就在这个tn对象上了。那tn对象到底是什么?

    继续阅读Toast源码,可以知道Toast其实是系统虚浮窗的一种具体表现形式,它的核心在于它的一个私有静态内部类class TN,它处理了Toast的显示以及隐藏。所以,我们可以通过反射获取这个TN对象,主动处理Toast的显示和隐藏,而不经过系统Service

    TN类源码:

    private static class TN extends ITransientNotification.Stub {

    final Runnable mShow = new Runnable() {

    @Override

    public void run() {

    handleShow();

    }

    };

    final Runnable mHide = new Runnable() {

    @Override

    public void run() {

    handleHide();

    // Don't do this in handleHide() because it is also invoked by handleShow()

    mNextView = null;

    }

    };

    ...

    final Handler mHandler = new Handler();

    ...

    View mView;

    View mNextView;

    WindowManager mWM;

    TN() {

    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;

    }

    /**

    * schedule handleShow into the right thread

    */

    @Override

    public void show() {

    if (localLOGV) Log.v(TAG, "SHOW: " + this);

    mHandler.post(mShow);

    }

    /**

    * schedule handleHide into the right thread

    */

    @Override

    public void hide() {

    if (localLOGV) Log.v(TAG, "HIDE: " + this);

    mHandler.post(mHide);

    }

    public void handleShow() {

    ...

    if (mView != mNextView) {

    // remove the old view if necessary

    handleHide();

    mView = mNextView;

    Context context = mView.getContext().getApplicationContext();

    if (context == null) {

    context = mView.getContext();

    }

    mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

    ...

    if (mView.getParent() != null) {

    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);

    mWM.removeView(mView);

    }

    ...

    mWM.addView(mView, mParams);

    ...

    }

    }

    private void trySendAccessibilityEvent() {...}

    public void handleHide() {

    ...

    if (mView != null) {

    // note: checking parent() just to make sure the view has

    // been added... i have seen cases where we get here when

    // the view isn't yet added, so let's try not to crash.

    if (mView.getParent() != null) {

    ...

    mWM.removeView(mView);

    }

    mView = null;

    }

    }

    }

    好吧,上面的代码太长不想看,那就把核心的代码挑出来

    public void show(){

    ...

    WindowManager mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

    mWN.addView(mView, mParams);

    }

    public void hide(){

    ...

    WindowManager mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

    mWN.removeView(mView);

    }

    所以,Toast的机制就是往WindowManager添加以及移除view,那只要获得TN对象,重新封装一次show()和hide()方法就可以实现自定义显示时间。

    private void initTN() {

    try {

    Field tnField = toast.getClass().getDeclaredField("mTN");

    tnField.setAccessible(true);

    mTN = (ITransientNotification) tnField.get(toast);

    /**调用tn.show()之前一定要先设置mNextView*/

    Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView");

    tnNextViewField.setAccessible(true);

    tnNextViewField.set(mTN, toast.getView());

    } catch (Exception e) {

    e.printStackTrace();

    }

    }

    public show(){

    initTN();

    mTN.show();

    }

    代码中mTN就是从Toast中利用反射获取的对象,类型是ITransientNotification,这是从android源码中拿出来的aidl接口,匹配TN的类型。主动调用mTN.show()方法后就会神奇的发现,Toast长时间存在屏幕中,即使离开了app它依然存在,直到调用mTN.hide()后才消失。

    Toast显示时间问题已经解决了,还有一个自定义动画的问题。现在回过头再看TN类的初始化方法代码,里面初始化了一个WindowManager.LayoutParams对象,做过悬浮窗功能的同学应该都接触过它,下面这一句代码就是定义窗口动画的关键,如果能修改params.windowAnimations就能够修改窗口动画。

    params.windowAnimations = com.android.internal.R.style.Animation_Toast;

    很不幸的是,params并不是一个公有的属性,那就暴力点继续用反射获取并且修改窗口动画

    private void initTN() {

    try {

    Field tnField = toast.getClass().getDeclaredField("mTN");

    tnField.setAccessible(true);

    mTN = (ITransientNotification) tnField.get(toast);

    /**调用tn.show()之前一定要先设置mNextView*/

    Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView");

    tnNextViewField.setAccessible(true);

    tnNextViewField.set(mTN, toast.getView());

    /**获取params后重新定义窗口动画*/

    Field tnParamsField = mTN.getClass().getDeclaredField("mParams");

    tnParamsField.setAccessible(true);

    WindowManager.LayoutParams params = (WindowManager.LayoutParams) tnParamsField.get(mTN);

    params.windowAnimations = R.style.anim_view;

    } catch (Exception e) {

    e.printStackTrace();

    }

    }

    Android黑科技:Toast.不需要权限的系统悬浮窗

    上面说到过,Toast其实就是系统悬浮窗的一种具体表现形式,那它跟普通的系统悬浮窗有什么区别呢?

    我们看看Android传统实现悬浮窗的代码:

    // 获取应用的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);

    大部分代码都在初始化WindowManager.LayoutParams对象上面了,对比一下Toast内部类TN中初始化的WindowManager.LayoutParams,不同的地方在于:

    // 类型

    params.type = WindowManager.LayoutParams.TYPE_TOAST;

    上面我们已经使用Toast实现了持久显示的悬浮窗,那普通悬浮窗和Toast悬浮窗除了type这个区别外,最大的区别就是Toast不需要权限!我们在应用中使用Toast的时候并没有设置什么额外的权限,但是传统使用悬浮窗的方式需要权限:

    以上这些都是在用户感知外的,只有开发者知道的区别。在用户感知内的区别目前知道的是Toast不能覆盖到系统status bar上面,而其他类型的悬浮窗大部分可以覆盖status bar,更多区别有待补充。

    有写的不对的地方请看官们指出

    展开全文
  • 前面几篇文章基本介绍完Activity上的窗口机制,但是我们常见的窗口就还有Dialog,Toast这些,本篇文章就来介绍这两个的窗口机制以及WindowManager.LayoutParams和Token WindowManager.LayoutParams 首先,先跟...

    转自:https://www.jianshu.com/p/bac61386d9bf 

    前面几篇文章基本介绍完Activity上的窗口机制,但是我们常见的窗口就还有Dialog,Toast这些,本篇文章就来介绍这两个的窗口机制以及WindowManager.LayoutParams和Token

    WindowManager.LayoutParams

    首先,先跟大家介绍这个WindowManager.LayoutParams,在前面几篇文章中,都有出现过这个LayoutParams,我们看下具体的源码。
    翻译参考

      public static class LayoutParams extends ViewGroup.LayoutParams
                implements Parcelable {
            //窗口的绝对XY位置,需要考虑gravity属性
            public int x;
            public int y;
            //在横纵方向上为相关的View预留多少扩展像素,如果是0则此view不能被拉伸,其他情况下扩展像素被widget均分
            public float horizontalWeight;
            public float verticalWeight;
            //窗口类型
            //有3种主要类型如下:
            //ApplicationWindows取值在FIRST_APPLICATION_WINDOW与LAST_APPLICATION_WINDOW之间,是常用的顶层应用程序窗口,须将token设置成Activity的token;
            //SubWindows取值在FIRST_SUB_WINDOW和LAST_SUB_WINDOW之间,与顶层窗口相关联,需将token设置成它所附着宿主窗口的token;
            //SystemWindows取值在FIRST_SYSTEM_WINDOW和LAST_SYSTEM_WINDOW之间,不能用于应用程序,使用时需要有特殊权限,它是特定的系统功能才能使用;
            public int type;
    
            //WindowType:开始应用程序窗口
            public static final int FIRST_APPLICATION_WINDOW = 1;
            //WindowType:所有程序窗口的base窗口,其他应用程序窗口都显示在它上面
            public static final int TYPE_BASE_APPLICATION   = 1;
            //WindowType:普通应用程序窗口,token必须设置为Activity的token来指定窗口属于谁
            public static final int TYPE_APPLICATION        = 2;
            //WindowType:应用程序启动时所显示的窗口,应用自己不要使用这种类型,它被系统用来显示一些信息,直到应用程序可以开启自己的窗口为止
            public static final int TYPE_APPLICATION_STARTING = 3;
            //WindowType:结束应用程序窗口
            public static final int LAST_APPLICATION_WINDOW = 99;
    
            //WindowType:SubWindows子窗口,子窗口的Z序和坐标空间都依赖于他们的宿主窗口
            public static final int FIRST_SUB_WINDOW        = 1000;
            //WindowType: 面板窗口,显示于宿主窗口的上层
            public static final int TYPE_APPLICATION_PANEL  = FIRST_SUB_WINDOW;
            //WindowType:媒体窗口(例如视频),显示于宿主窗口下层
            public static final int TYPE_APPLICATION_MEDIA  = FIRST_SUB_WINDOW+1;
            //WindowType:应用程序窗口的子面板,显示于所有面板窗口的上层
            public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
            //WindowType:对话框,类似于面板窗口,绘制类似于顶层窗口,而不是宿主的子窗口
            public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
            //WindowType:媒体信息,显示在媒体层和程序窗口之间,需要实现半透明效果
            public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW+4;
            //WindowType:子窗口结束
            public static final int LAST_SUB_WINDOW         = 1999;
    
            //WindowType:系统窗口,非应用程序创建
            public static final int FIRST_SYSTEM_WINDOW     = 2000;
            //WindowType:状态栏,只能有一个状态栏,位于屏幕顶端,其他窗口都位于它下方
            public static final int TYPE_STATUS_BAR         = FIRST_SYSTEM_WINDOW;
            //WindowType:搜索栏,只能有一个搜索栏,位于屏幕上方
            public static final int TYPE_SEARCH_BAR         = FIRST_SYSTEM_WINDOW+1;
            //WindowType:电话窗口,它用于电话交互(特别是呼入),置于所有应用程序之上,状态栏之下
            public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
            //WindowType:系统提示,出现在应用程序窗口之上
            public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
            //WindowType:锁屏窗口
            public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
            //WindowType:信息窗口,用于显示Toast
            public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
            //WindowType:系统顶层窗口,显示在其他一切内容之上,此窗口不能获得输入焦点,否则影响锁屏
            public static final int TYPE_SYSTEM_OVERLAY     = FIRST_SYSTEM_WINDOW+6;
            //WindowType:电话优先,当锁屏时显示,此窗口不能获得输入焦点,否则影响锁屏
            public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
            //WindowType:系统对话框
            public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
            //WindowType:锁屏时显示的对话框
            public static final int TYPE_KEYGUARD_DIALOG    = FIRST_SYSTEM_WINDOW+9;
            //WindowType:系统内部错误提示,显示于所有内容之上
            public static final int TYPE_SYSTEM_ERROR       = FIRST_SYSTEM_WINDOW+10;
            //WindowType:内部输入法窗口,显示于普通UI之上,应用程序可重新布局以免被此窗口覆盖
            public static final int TYPE_INPUT_METHOD       = FIRST_SYSTEM_WINDOW+11;
            //WindowType:内部输入法对话框,显示于当前输入法窗口之上
            public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
            //WindowType:墙纸窗口
            public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
            //WindowType:状态栏的滑动面板
            public static final int TYPE_STATUS_BAR_PANEL   = FIRST_SYSTEM_WINDOW+14;
            //WindowType:安全系统覆盖窗口,这些窗户必须不带输入焦点,否则会干扰键盘
            public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
            //WindowType:拖放伪窗口,只有一个阻力层(最多),它被放置在所有其他窗口上面
            public static final int TYPE_DRAG               = FIRST_SYSTEM_WINDOW+16;
            //WindowType:状态栏下拉面板
            public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
            //WindowType:鼠标指针
            public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
            //WindowType:导航栏(有别于状态栏时)
            public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
            //WindowType:音量级别的覆盖对话框,显示当用户更改系统音量大小
            public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;
            //WindowType:起机进度框,在一切之上
            public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;
            //WindowType:假窗,消费导航栏隐藏时触摸事件
            public static final int TYPE_HIDDEN_NAV_CONSUMER = FIRST_SYSTEM_WINDOW+22;
            //WindowType:梦想(屏保)窗口,略高于键盘
            public static final int TYPE_DREAM = FIRST_SYSTEM_WINDOW+23;
            //WindowType:导航栏面板(不同于状态栏的导航栏)
            public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;
            //WindowType:universe背后真正的窗户
            public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25;
            //WindowType:显示窗口覆盖,用于模拟辅助显示设备
            public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;
            //WindowType:放大窗口覆盖,用于突出显示的放大部分可访问性放大时启用
            public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;
            //WindowType:......
            public static final int TYPE_KEYGUARD_SCRIM           = FIRST_SYSTEM_WINDOW+29;
            public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;
            public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;
            public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;
            //WindowType:系统窗口结束
            public static final int LAST_SYSTEM_WINDOW      = 2999;
    
            //MemoryType:窗口缓冲位于主内存
            public static final int MEMORY_TYPE_NORMAL = 0;
            //MemoryType:窗口缓冲位于可以被DMA访问,或者硬件加速的内存区域
            public static final int MEMORY_TYPE_HARDWARE = 1;
            //MemoryType:窗口缓冲位于可被图形加速器访问的区域
            public static final int MEMORY_TYPE_GPU = 2;
            //MemoryType:窗口缓冲不拥有自己的缓冲区,不能被锁定,缓冲区由本地方法提供
            public static final int MEMORY_TYPE_PUSH_BUFFERS = 3;
    
            //指出窗口所使用的内存缓冲类型,默认为NORMAL 
            public int memoryType;
    
            //Flag:当该window对用户可见的时候,允许锁屏
            public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON     = 0x00000001;
            //Flag:让该window后所有的东西都成暗淡
            public static final int FLAG_DIM_BEHIND        = 0x00000002;
            //Flag:让该window后所有东西都模糊(4.0以上已经放弃这种毛玻璃效果)
            public static final int FLAG_BLUR_BEHIND        = 0x00000004;
            //Flag:让window不能获得焦点,这样用户快就不能向该window发送按键事
            public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;
            //Flag:让该window不接受触摸屏事件
            public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;
            //Flag:即使在该window在可获得焦点情况下,依旧把该window之外的任何event发送到该window之后的其他window
            public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;
            //Flag:当手机处于睡眠状态时,如果屏幕被按下,那么该window将第一个收到
            public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
            //Flag:当该window对用户可见时,让设备屏幕处于高亮(bright)状态
            public static final int FLAG_KEEP_SCREEN_ON     = 0x00000080;
            //Flag:让window占满整个手机屏幕,不留任何边界
            public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
            //Flag:window大小不再不受手机屏幕大小限制,即window可能超出屏幕之外
            public static final int FLAG_LAYOUT_NO_LIMITS   = 0x00000200;
            //Flag:window全屏显示
            public static final int FLAG_FULLSCREEN      = 0x00000400;
            //Flag:恢复window非全屏显示
            public static final int FLAG_FORCE_NOT_FULLSCREEN   = 0x00000800;
            //Flag:开启抖动(dithering)
            public static final int FLAG_DITHER             = 0x00001000;
            //Flag:当该window在进行显示的时候,不允许截屏
            public static final int FLAG_SECURE             = 0x00002000;
            //Flag:一个特殊模式的布局参数用于执行扩展表面合成时到屏幕上
            public static final int FLAG_SCALED             = 0x00004000;
            //Flag:用于windows时,经常会使用屏幕用户持有反对他们的脸,它将积极过滤事件流,以防止意外按在这种情况下,可能不需要为特定的窗口,在检测到这样一个事件流时,应用程序将接收取消运动事件表明,这样应用程序可以处理这相应地采取任何行动的事件,直到手指释放
            public static final int FLAG_IGNORE_CHEEK_PRESSES    = 0x00008000;
            //Flag:一个特殊的选项只用于结合FLAG_LAYOUT_IN_SC
            public static final int FLAG_LAYOUT_INSET_DECOR = 0x00010000;
            //Flag:转化的状态FLAG_NOT_FOCUSABLE对这个窗口当前如何进行交互的方法
            public static final int FLAG_ALT_FOCUSABLE_IM = 0x00020000;
            //Flag:如果你设置了该flag,那么在你FLAG_NOT_TOUNCH_MODAL的情况下,即使触摸屏事件发送在该window之外,其事件被发送到了后面的window,那么该window仍然将以MotionEvent.ACTION_OUTSIDE形式收到该触摸屏事件
            public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;
            //Flag:当锁屏的时候,显示该window
            public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
            //Flag:在该window后显示系统的墙纸
            public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
            //Flag:当window被显示的时候,系统将把它当做一个用户活动事件,以点亮手机屏幕
            public static final int FLAG_TURN_SCREEN_ON = 0x00200000;
            //Flag:消失键盘
            public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;
            //Flag:当该window在可以接受触摸屏情况下,让因在该window之外,而发送到后面的window的触摸屏可以支持split touch
            public static final int FLAG_SPLIT_TOUCH = 0x00800000;
            //Flag:对该window进行硬件加速,该flag必须在Activity或Dialog的Content View之前进行设置
            public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000;
            //Flag:让window占满整个手机屏幕,不留任何边界
            public static final int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000;
            //Flag:请求一个半透明的状态栏背景以最小的系统提供保护
            public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;
            //Flag:请求一个半透明的导航栏背景以最小的系统提供保护
            public static final int FLAG_TRANSLUCENT_NAVIGATION = 0x08000000;
            //Flag:......
            public static final int FLAG_LOCAL_FOCUS_MODE = 0x10000000;
            public static final int FLAG_SLIPPERY = 0x20000000;
            public static final int FLAG_LAYOUT_ATTACHED_IN_DECOR = 0x40000000;
            public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000;
    
            //行为选项标记
            public int flags;
    
            //PrivateFlags:......
            public static final int PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED = 0x00000001;
            public static final int PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED = 0x00000002;
            public static final int PRIVATE_FLAG_WANTS_OFFSET_NOTIFICATIONS = 0x00000004;
            public static final int PRIVATE_FLAG_SHOW_FOR_ALL_USERS = 0x00000010;
            public static final int PRIVATE_FLAG_NO_MOVE_ANIMATION = 0x00000040;
            public static final int PRIVATE_FLAG_COMPATIBLE_WINDOW = 0x00000080;
            public static final int PRIVATE_FLAG_SYSTEM_ERROR = 0x00000100;
            public static final int PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR = 0x00000200;
            public static final int PRIVATE_FLAG_KEYGUARD = 0x00000400;
            public static final int PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS = 0x00000800;
    
            //私有的行为选项标记
            public int privateFlags;
    
            public static final int NEEDS_MENU_UNSET = 0;
            public static final int NEEDS_MENU_SET_TRUE = 1;
            public static final int NEEDS_MENU_SET_FALSE = 2;
            public int needsMenuKey = NEEDS_MENU_UNSET;
    
            public static boolean mayUseInputMethod(int flags) {
                ......
            }
    
            //SOFT_INPUT:用于描述软键盘显示规则的bite的mask
            public static final int SOFT_INPUT_MASK_STATE = 0x0f;
            //SOFT_INPUT:没有软键盘显示的约定规则
            public static final int SOFT_INPUT_STATE_UNSPECIFIED = 0;
            //SOFT_INPUT:可见性状态softInputMode,请不要改变软输入区域的状态
            public static final int SOFT_INPUT_STATE_UNCHANGED = 1;
            //SOFT_INPUT:用户导航(navigate)到你的窗口时隐藏软键盘
            public static final int SOFT_INPUT_STATE_HIDDEN = 2;
            //SOFT_INPUT:总是隐藏软键盘
            public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;
            //SOFT_INPUT:用户导航(navigate)到你的窗口时显示软键盘
            public static final int SOFT_INPUT_STATE_VISIBLE = 4;
            //SOFT_INPUT:总是显示软键盘
            public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;
            //SOFT_INPUT:显示软键盘时用于表示window调整方式的bite的mask
            public static final int SOFT_INPUT_MASK_ADJUST = 0xf0;
            //SOFT_INPUT:不指定显示软件盘时,window的调整方式
            public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;
            //SOFT_INPUT:当显示软键盘时,调整window内的控件大小以便显示软键盘
            public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;
            //SOFT_INPUT:当显示软键盘时,调整window的空白区域来显示软键盘,即使调整空白区域,软键盘还是有可能遮挡一些有内容区域,这时用户就只有退出软键盘才能看到这些被遮挡区域并进行
            public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
            //SOFT_INPUT:当显示软键盘时,不调整window的布局
            public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;
            //SOFT_INPUT:用户导航(navigate)到了你的window
            public static final int SOFT_INPUT_IS_FORWARD_NAVIGATION = 0x100;
    
            //软输入法模式选项
            public int softInputMode;
    
            //窗口如何停靠
            public int gravity;
            //水平边距,容器与widget之间的距离,占容器宽度的百分率
            public float horizontalMargin;
            //纵向边距
            public float verticalMargin;
            //积极的insets绘图表面和窗口之间的内容
            public final Rect surfaceInsets = new Rect();
            //期望的位图格式,默认为不透明,参考android.graphics.PixelFormat
            public int format;
            //窗口所使用的动画设置,它必须是一个系统资源而不是应用程序资源,因为窗口管理器不能访问应用程序
            public int windowAnimations;
            //整个窗口的半透明值,1.0表示不透明,0.0表示全透明
            public float alpha = 1.0f;
            //当FLAG_DIM_BEHIND设置后生效,该变量指示后面的窗口变暗的程度,1.0表示完全不透明,0.0表示没有变暗
            public float dimAmount = 1.0f;
    
            public static final float BRIGHTNESS_OVERRIDE_NONE = -1.0f;
            public static final float BRIGHTNESS_OVERRIDE_OFF = 0.0f;
            public static final float BRIGHTNESS_OVERRIDE_FULL = 1.0f;
            public float screenBrightness = BRIGHTNESS_OVERRIDE_NONE;
            //用来覆盖用户设置的屏幕亮度,表示应用用户设置的屏幕亮度,从0到1调整亮度从暗到最亮发生变化
            public float buttonBrightness = BRIGHTNESS_OVERRIDE_NONE;
    
            public static final int ROTATION_ANIMATION_ROTATE = 0;
            public static final int ROTATION_ANIMATION_CROSSFADE = 1;
            public static final int ROTATION_ANIMATION_JUMPCUT = 2;
            //定义出入境动画在这个窗口旋转设备时使用
            public int rotationAnimation = ROTATION_ANIMATION_ROTATE;
    
            //窗口的标示符
            public IBinder token = null;
            //此窗口所在的包名
            public String packageName = null;
            //屏幕方向
            public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
            //首选的刷新率的窗口
            public float preferredRefreshRate;
            //控制status bar是否显示
            public int systemUiVisibility;
            //ui能见度所请求的视图层次结构
            public int subtreeSystemUiVisibility;
            //得到关于系统ui能见度变化的回调
            public boolean hasSystemUiListeners;
    
            public static final int INPUT_FEATURE_DISABLE_POINTER_GESTURES = 0x00000001;
            public static final int INPUT_FEATURE_NO_INPUT_CHANNEL = 0x00000002;
            public static final int INPUT_FEATURE_DISABLE_USER_ACTIVITY = 0x00000004;
            public int inputFeatures;
            public long userActivityTimeout = -1;
    
            ......
            public final int copyFrom(LayoutParams o) {
                ......
            }
    
            ......
            public void scale(float scale) {
                ......
            }
    
            ......
        }
    

    可以看到在WindowManager.LayoutParams上有三种窗口类型type,对应为

    • 应用程序窗口 : type值在 FIRST_APPLICATION_WINDOW ~ LAST_APPLICATION_WINDOW 须将token设置成Activity的token
      eg: 前面介绍的Activity窗口,Dialog
    • 子窗口: type值在 FIRST_SUB_WINDOW ~ LAST_SUB_WINDOW SubWindows与顶层窗口相关联,需将token设置成它所附着宿主窗口的token
      eg: PopupWindow(想要依附在Activity上需要将token设置成Activity的token)
    • 系统窗口: type值在 FIRST_SYSTEM_WINDOW ~ LAST_SYSTEM_WINDOW ** SystemWindows不能用于应用程序,使用时需要有特殊权限,它是特定的系统功能才能使用。
      eg: Toast,输入法等。

    WindowManager.LayoutParams源码中也讲到输入法的问题,里面有很多种模式,通过设置softInputMode来调整输入法。这里举个常见例子吧,平时我们在Activity的底部放置EditText的时候,输入法的弹出可能会遮挡住界面。
    这里通过设置相应的softInputMode就可以解决这个问题

    <activity  
         android:name=".TestActivity"  
         android:windowSoftInputMode="stateVisible|adjustResize" >  
         <intent-filter>  
              <action android:name="android.intent.action.MAIN" />  
              <category android:name="android.intent.category.LAUNCHER" />  
         </intent-filter>  
    </activity>  
    

    或者

    public class TestActivity extends AppCompatActivity {      
        @Override  
        protected void onCreate(Bundle savedInstanceState) {  
            super.onCreate(savedInstanceState);                 
            getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE|WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
            setContentView(R.layout.activity_test);  
        }
    }  
    

    另外,三种类型里面出现了个概念,就是token问题。
    在应用程序窗口中,token是用来标识Activity的,一个Activity就对应一个token令牌
    而在子窗口中,某个子窗口想要依附在对应的宿主窗口上设置要将token设置为对应宿主窗口的token。

    token

    token是用来表示窗口的一个令牌,只有符合条件的token才能被WMS通过添加到应用上。
    我们来看下token的传递过程

    首先对于Activity里面的token,它的创建则是在AMS启动Activity开始的,之后保存在ActivityRecord.appToken中。而对于Activity中的token绑定到对应的Window上
    我们知道,应用程序窗口的Activity窗口Window是在Activity创建过程中创建的,具体是在activity.attach方法中创建的。

     final void attach(Context context, ActivityThread aThread,
                Instrumentation instr, IBinder token, int ident,
                Application application, Intent intent, ActivityInfo info,
                CharSequence title, Activity parent, String id,
                NonConfigurationInstances lastNonConfigurationInstances,
                Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
            ...
            mWindow = new PhoneWindow(this);
            mWindow.setCallback(this);
            mWindow.setOnWindowDismissedCallback(this);
            mWindow.getLayoutInflater().setPrivateFactory(this);
            //设置软键盘
            if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
                mWindow.setSoftInputMode(info.softInputMode);
            }
            ...
            mWindow.setWindowManager(
                    (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                    mToken, mComponent.flattenToString(),
                    (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
            ...
            mWindowManager = mWindow.getWindowManager();
        }
    

    追踪token可看到最后传递到window.setWindowManager中

        public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
                boolean hardwareAccelerated) {
            mAppToken = appToken;
            mAppName = appName;
            mHardwareAccelerated = hardwareAccelerated
                    || SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
            if (wm == null) {
                wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
            }
            mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
        }
    

    在setWindowManager中,appToken赋值到Window上,同时在当前Window上创建了WindowManager。

    在将DecorView添加到WindowManager时候,会调用到windowManagerGlobal.addView方法

     public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
           ...
            final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
            if (parentWindow != null) {
                parentWindow.adjustLayoutParamsForSubWindow(wparams);
            } else {
               ...
            }
           ...
        }
    
    

    parentWindow.adjustLayoutParamsForSubWindow(wparams);方法里面的重要一步就是给token设置值。不过在这以前要判断parentWindow是否为null。

    • 如果是应用程序窗口的话,这个parentWindow就是activity的window
    • 如果是子窗口的话,这个parentWindow就是activity的window
    • 如果是系统窗口的话,那个parentWindow就是null

    这个parentWindow则是在创建WindowManagerImpl的时候被赋值的

     private WindowManagerImpl(Display display, Window parentWindow) {
            mDisplay = display;
            mParentWindow = parentWindow;
        }
    

    为什么说子窗口中的parentWindow是Activity的window,因为子窗口中用到的是Activity的WindowManager,这里会在下面分析到Dialog的时候说。
    在Window.adjustLayoutParamsForSubWindow方法中

    void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
            CharSequence curTitle = wp.getTitle();
            if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
                if (wp.token == null) {
                    View decor = peekDecorView();
                    if (decor != null) {
                        wp.token = decor.getWindowToken();
                    }
                }
                ...
            } else {
                if (wp.token == null) {
                    wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
                }
               ...
            }
         ...
        }
    

    可以看到在adjustLayoutParamsForSubWindow通过wp.type来判断当前窗口的类型,如果是子窗口类型,则wp.token = decor.getWindowToken();这里赋值的是父窗口的W对象。关于W对象在下面讲解。
    如果是应用程序窗口,则走分支。一般应用程序窗口的话,mContainer为null,也就是mAppToken,就是Activity的mToken对象。

    获取到Token后就保存在了LayoutParams里面,接着到WindowManagerGlobal.addView中去。

       root = new ViewRootImpl(view.getContext(), display);
    
       view.setLayoutParams(wparams);
    
       mViews.add(view);
       mRoots.add(root);
       mParams.add(wparams);
       ...
       root.setView(view, wparams, panelParentView);</pre>
    

    可以看到token保存在WindowManager.LayoutParams中,之后再传到了ViewRootImpl.setView

    public final class ViewRootImpl implements ViewParent,
            View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
            
            final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();
            final IWindowSession mWindowSession;
            ...
            public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
            synchronized (this) {
                if (mView == null) {
                    mView = view;
                    ...
                    mWindowAttributes.copyFrom(attrs);
                    ...
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                                getHostVisibility(), mDisplay.getDisplayId(),
                                mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                                mAttachInfo.mOutsets, mInputChannel);
                }
            }
        }
    }
    

    可以看到从WindowManagerGlobal中传递过来的params赋值到了ViewRootImpl中的mWindowAttributes中,之后调用到了ViewRootImpl.setView方法中的mWindowSession的addToDisplay方法,该方法用来请求WMS添加Window
    mWindowSession的类型是IWindowSession它的实现类是Session,用来与WMS通信

     final class Session extends IWindowSession.Stub
            implements IBinder.DeathRecipient {
          final WindowManagerService mService;
          ...
        @Override
        public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,Rect outOutsets, InputChannel outInputChannel) {
            return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,outContentInsets, outStableInsets, outOutsets, outInputChannel);
        }
     }
    

    我们看下WindowManagerService中是如何判断这个token的

     public int addWindow(Session session, IWindow client, int seq,
                WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
                Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
                InputChannel outInputChannel) {
            int[] appOp = new int[1];
            //判断权限
            int res = mPolicy.checkAddPermission(attrs, appOp);
            if (res != WindowManagerGlobal.ADD_OKAY) {
                return res;
            }
            ...
            final int type = attrs.type;
            synchronized(mWindowMap) {
                ...
                boolean addToken = false;
                WindowToken token = mTokenMap.get(attrs.token);
                if (token == null) {
                    if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                        Slog.w(TAG, "Attempted to add application window with unknown token "
                              + attrs.token + ".  Aborting.");
                        return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                    if (type == TYPE_INPUT_METHOD) {
                        Slog.w(TAG, "Attempted to add input method window with unknown token "
                              + attrs.token + ".  Aborting.");
                        return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                    if (type == TYPE_VOICE_INTERACTION) {
                        Slog.w(TAG, "Attempted to add voice interaction window with unknown token "
                              + attrs.token + ".  Aborting.");
                        return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                    if (type == TYPE_WALLPAPER) {
                        Slog.w(TAG, "Attempted to add wallpaper window with unknown token "
                              + attrs.token + ".  Aborting.");
                        return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                    if (type == TYPE_DREAM) {
                        Slog.w(TAG, "Attempted to add Dream window with unknown token "
                              + attrs.token + ".  Aborting.");
                        return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                    if (type == TYPE_ACCESSIBILITY_OVERLAY) {
                        Slog.w(TAG, "Attempted to add Accessibility overlay window with unknown token "
                                + attrs.token + ".  Aborting.");
                        return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                    token = new WindowToken(this, attrs.token, -1, false);
                    addToken = true;
                } else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
                    AppWindowToken atoken = token.appWindowToken;
                    if (atoken == null) {
                        Slog.w(TAG, "Attempted to add window with non-application token "
                              + token + ".  Aborting.");
                        return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
                    } else if (atoken.removed) {
                        Slog.w(TAG, "Attempted to add window with exiting application token "
                              + token + ".  Aborting.");
                        return WindowManagerGlobal.ADD_APP_EXITING;
                    }
                    if (type == TYPE_APPLICATION_STARTING && atoken.firstWindowDrawn) {
                        // No need for this guy!
                        if (localLOGV) Slog.v(
                                TAG, "**** NO NEED TO START: " + attrs.getTitle());
                        return WindowManagerGlobal.ADD_STARTING_NOT_NEEDED;
                    }
                } else if (type == TYPE_INPUT_METHOD) {
                    if (token.windowType != TYPE_INPUT_METHOD) {
                        Slog.w(TAG, "Attempted to add input method window with bad token "
                                + attrs.token + ".  Aborting.");
                          return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                } else if (type == TYPE_VOICE_INTERACTION) {
                    if (token.windowType != TYPE_VOICE_INTERACTION) {
                        Slog.w(TAG, "Attempted to add voice interaction window with bad token "
                                + attrs.token + ".  Aborting.");
                          return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                } else if (type == TYPE_WALLPAPER) {
                    if (token.windowType != TYPE_WALLPAPER) {
                        Slog.w(TAG, "Attempted to add wallpaper window with bad token "
                                + attrs.token + ".  Aborting.");
                          return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                } else if (type == TYPE_DREAM) {
                    if (token.windowType != TYPE_DREAM) {
                        Slog.w(TAG, "Attempted to add Dream window with bad token "
                                + attrs.token + ".  Aborting.");
                          return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                } else if (type == TYPE_ACCESSIBILITY_OVERLAY) {
                    if (token.windowType != TYPE_ACCESSIBILITY_OVERLAY) {
                        Slog.w(TAG, "Attempted to add Accessibility overlay window with bad token "
                                + attrs.token + ".  Aborting.");
                        return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                    }
                } else if (token.appWindowToken != null) {
                    Slog.w(TAG, "Non-null appWindowToken for system window of type=" + type);
                    // It is not valid to use an app token with other system types; we will
                    // instead make a new token for it (as if null had been passed in for the token).
                    attrs.token = null;
                    token = new WindowToken(this, null, -1, false);
                    addToken = true;
                }
            ...
            return res;
        }
    
    

    可以看到在WMS中,做了很多的判断,显示判断对应的权限,如果不满足则直接return到ViewRootImpl,如果满足权限,则在mWindowMap中去匹配params.token值,如果不满足,则return对应的错误。都没问题则开始添加Window。
    而在ViewRootImpl则有判断对应的返回值来报错

      public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
          ...
          res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                                getHostVisibility(), mDisplay.getDisplayId(),
                                mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                                mAttachInfo.mOutsets, mInputChannel);
          if (res < WindowManagerGlobal.ADD_OKAY) {
                        mAttachInfo.mRootView = null;
                        mAdded = false;
                        mFallbackEventHandler.setView(null);
                        unscheduleTraversals();
                        setAccessibilityFocus(null, null);
                        switch (res) {
                            case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                            case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                                throw new WindowManager.BadTokenException(
                                        "Unable to add window -- token " + attrs.token
                                        + " is not valid; is your activity running?");
                            case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                                throw new WindowManager.BadTokenException(
                                        "Unable to add window -- token " + attrs.token
                                        + " is not for an application");
                            case WindowManagerGlobal.ADD_APP_EXITING:
                                throw new WindowManager.BadTokenException(
                                        "Unable to add window -- app for token " + attrs.token
                                        + " is exiting");
                           ...
                    }
    

    可以看到ViewRootImpl会根据WMS检测token返回对应的情况,再去判断是否报错。

    ViewRootImpl 和View的mAttachInfo

    token与View的绑定,前面讲到的token则是绑定在对应的Window,而对于View而言,它的所有绑定信息都是存在一个静态内部类AttachInfo中
    在ViewRootImpl的创建中,可以看到

    public ViewRootImpl(Context context, Display display) {
            mContext = context;
            mWindowSession = WindowManagerGlobal.getWindowSession();
            ...
            mWindow = new W(this);
            ...
            mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
            ...
        }
    

    可以看到,这里传递了mWindow和mWindowSession,而这里赋值的mWindow对象,是通过new W(ViewRootImpl v)创建出来的,有留意的话,会发现在向WMS请求添加窗口,也就是在addToDisplay中,传递了mWindow这个参数

    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                                getHostVisibility(), mDisplay.getDisplayId(),
                                mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                                mAttachInfo.mOutsets, mInputChannel);
    

    这个mWindow也可以说是token,可以通过mWindow.asBinder()拿到。它是WMS回调的接口。

     static class W extends IWindow.Stub {
            private final WeakReference<ViewRootImpl> mViewAncestor;
            private final IWindowSession mWindowSession;
    
            W(ViewRootImpl viewAncestor) {
                mViewAncestor = new WeakReference<ViewRootImpl>(viewAncestor);
                mWindowSession = viewAncestor.mWindowSession;
            }
            ...
     }
    

    在AttachInfo的构造参数中

    final static class AttachInfo {
            final IWindowSession mSession;
            final IWindow mWindow;
            final IBinder mWindowToken;
            AttachInfo(IWindowSession session, IWindow window, Display display,
                    ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer) {
                mSession = session;
                mWindow = window;
                mWindowToken = window.asBinder();
                mDisplay = display;
                mViewRootImpl = viewRootImpl;
                mHandler = handler;
                mRootCallbacks = effectPlayer;
            }
            ....
    }
    

    IWindow mWindow:WMS回调应用程序的Binder接口
    WindowSession mSession : 就是访问wms的Binder接口
    IBinder mWindowToken : 它的赋值是window.asBinder,代表的是W对象,IWindow是通过new W创建的。mWindowToken也是WMS和应用程序交互的Binder接口。获取到后就可以通过view.getWindowToken获取

    可以说AttachInfo代表了一系列绑定的状态信息,接着通过ViewRootImpl赋值到每个Window上的View上,如何赋值呢?
    在ViewRootImpl的setView过程中,调用到了View的绘制performTraversals,这些前几篇有讲过,在这个方法中

    private void performTraversals() {
            // cache mView since it is used so much below...
            final View host = mView;
            ...
            host.dispatchAttachedToWindow(mAttachInfo, 0);
            ...
    }
    

    而在View这个方法中

     void dispatchAttachedToWindow(AttachInfo info, int visibility) {
            //System.out.println("Attached! " + this);
            mAttachInfo = info;
            if (mOverlay != null) {
                mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
            }
            ...
     }
    

    会判断是否是ViewGroup,如果是,则调用到ViewGroup里面的方法

      void dispatchAttachedToWindow(AttachInfo info, int visibility) {
            mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
            super.dispatchAttachedToWindow(info, visibility);
            mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
    
            final int count = mChildrenCount;
            final View[] children = mChildren;
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                child.dispatchAttachedToWindow(info,
                        combineVisibility(visibility, child.getVisibility()));
            }
           ...
      }
    

    可以看到在这个方法中遍历调用了dispatchAttachedToWindow去赋值AttachInfo,而这些AttachInfo在同一个ViewGroup则是相同的值。之后View就获得了这些绑定信息。

    Dialog

    好了,做了那么多铺垫,可以开始看其他窗口了。
    先看下Dialog的创建

     Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
            if (createContextThemeWrapper) {
                if (themeResId == 0) {
                    final TypedValue outValue = new TypedValue();
                    context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                    themeResId = outValue.resourceId;
                }
                mContext = new ContextThemeWrapper(context, themeResId);
            } else {
                mContext = context;
            }
    
            mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    
            final Window w = new PhoneWindow(mContext);
            mWindow = w;
            w.setCallback(this);
            w.setOnWindowDismissedCallback(this);
            w.setWindowManager(mWindowManager, null, null);
            w.setGravity(Gravity.CENTER);
    
            mListenersHandler = new ListenersHandler(this);
        }
    

    我们知道,关于Dialog的创建过程中要传入参数Activity,主要是Dialog的创建过程与Activity相似,它同时也需要一些主题资源也就是ContextThemeWrapper,但是Dialog只是一个类,它并没有继承于ContextThemeWrapper,顾也就需要继承于ContextThemeWrapper的Activity来结合使用。

    可以看到,在上面的构造方法中,传入的Context的是Activity的Context,接着获取了一个WindowManager,这里的WindowManager是通过context.getSystemService(Context.WINDOW_SERVICE),而这里的Context是Activity,我们看下Activity里面这个方法

     @Override
        public Object getSystemService(@ServiceName @NonNull String name) {
            ...
            if (WINDOW_SERVICE.equals(name)) {
                return mWindowManager;
            } else if (SEARCH_SERVICE.equals(name)) {
                ensureSearchManager();
                return mSearchManager;
            }
            return super.getSystemService(name);
        }
    

    可以看到这里返回的WindowManager也就是Activity的WindowManager。
    接着创建了一个新的Window,类型是PhoneWindow,与Activity的Window相比,是不同的对象。Dialog与Activity,同个WindowManager,不同Window。接着设置了Callback接口回调,这也是Dialog能够接受到按键事件的原因。接着调用setWindowManager设置到Window中。注意这个方法参数:这里第二个参数传递的是null,也就是token为null

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName){
        ...
    }
    

    token居然为null,那么Dialog到底是如何依附在Activity上的,我们看下show方法

    public void show() {
            ...
            if (!mCreated) {
                dispatchOnCreate(null);
            }
    
            onStart();
            mDecor = mWindow.getDecorView();
            ...
            WindowManager.LayoutParams l = mWindow.getAttributes();
            ...
            try {
                mWindowManager.addView(mDecor, l);
                mShowing = true;
       
                sendShowMessage();
            } finally {
            }
        }
    

    可以看到,show方法会先调用dispatchOnCreate来创建,最后会调用到onCreate

    /**
         * Similar to {@link Activity#onCreate}, you should initialize your dialog
         * in this method, including calling {@link #setContentView}.
         * @param savedInstanceState If this dialog is being reinitalized after a
         *     the hosting activity was previously shut down, holds the result from
         *     the most recent call to {@link #onSaveInstanceState}, or null if this
         *     is the first time.
         */
        protected void onCreate(Bundle savedInstanceState) {
        }
    

    可以看到,这个跟Activity相似,只不过在这里Dialog是空的,但是它的子类,AlertDialog这些都是重写了它,既然与Activity相似,那也就需要setContentView(这个很重要)了。
    接着调用到

    mDecor = mWindow.getDecorView();
    

    这个mWindow就是前面创建的PhoneWindow实例,前面明明没创建DecorView啊,为啥这里Window能得到DecorView啊。咦,没错,你可能会猜到是在setContentView中创建的,因为这点跟Activity很像。

     public void setContentView(@LayoutRes int layoutResID) {
            mWindow.setContentView(layoutResID);
        }
    

    可以看到这里调用到了window.setContentView,而在第二篇文章也讲过,Activity.setContentView,实际上也是调用到了window.setContentView,在它的实现类PhoneWindow.setContentView中就会创建DecorView了。

    接着到了

     WindowManager.LayoutParams l = mWindow.getAttributes();
    

    看下getAttributes

     public final WindowManager.LayoutParams getAttributes() {
            return mWindowAttributes;
        }
    

    mWindowAttributes则是Window的一个成员变量

    private final WindowManager.LayoutParams mWindowAttributes =
            new WindowManager.LayoutParams();
    

    可以看到这里是个默认创建

    public LayoutParams() {
                super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
                type = TYPE_APPLICATION;
                format = PixelFormat.OPAQUE;
            }
    

    可以看到这里的类型是应用程序类型,对应WindowManager.LayoutParams说明是

    //WindowType:普通应用程序窗口,token必须设置为Activity的token来指定窗口属于谁
    public static final int TYPE_APPLICATION        = 2;
    

    接着调用到了 mWindowManager.addView(mDecor, l); 不过这里调用到的是Activity的WindowManager,之后就到了WindowGlobal.addView,ViewRootImpl.setView,addToDisplay。和Activity的添加一致。也就是说因为传入的参数是Activity的context,使得在添加窗口的时调用的是Activity的WindowManager,而Activity的WindowManager则保存了对应的token,所以Dialog才可以被添加。如果此时传递的是getApplication或者是Service,则在ViewRootImpl.setView中会报错,找不到对应的token,这也就是我们设置Dialog的时候要传递Activity的原因。

    Toast

    讲完了Dialog就来讲讲一个系统窗口Toast,它与Dialog,Activity不同。我们从平常用法开始

    Toast.makeText(MainActivity.this , "Hohohong" , Toast.LENGTH_SHORT);
    

    在Toast的makeText方法中

    /**
         * Make a standard toast that just contains a text view.
         *
         * @param context  The context to use.  Usually your {@link android.app.Application}
         *                 or {@link android.app.Activity} object.
         * @param text     The text to show.  Can be formatted text.
         * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
         *                 {@link #LENGTH_LONG}
         *
         */
        public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
            Toast result = new Toast(context);
    
            LayoutInflater inflate = (LayoutInflater)
                    context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
            TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
            tv.setText(text);
            
            result.mNextView = v;
            result.mDuration = duration;
    
            return result;
        }
    

    可以看到这里Context的说明允许Application或者Activity了,接着创建Toast对象,实例化默认的布局。
    我们看下构造方法

       public Toast(Context context) {
            mContext = context;
            mTN = new TN();
            mTN.mY = context.getResources().getDimensionPixelSize(
                    com.android.internal.R.dimen.toast_y_offset);
            mTN.mGravity = context.getResources().getInteger(
                    com.android.internal.R.integer.config_toastDefaultGravity);
        }
    

    在构造方法中,创建了TN对象,这个TN又是什么

     private static class TN extends ITransientNotification.Stub {
            final Runnable mShow = new Runnable() {
                @Override
                public void run() {
                    handleShow();
                }
            };
    
            final Runnable mHide = new Runnable() {
                @Override
                public void run() {
                    handleHide();
                    // Don't do this in handleHide() because it is also invoked by handleShow()
                    mNextView = null;
                }
            };
    
            private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
            ...
    
            WindowManager mWM;
            TN() {
                // XXX This should be changed to use a Dialog, with a Theme.Toast
                // defined that sets up the layout params appropriately.
                final WindowManager.LayoutParams params = mParams;
                params.windowAnimations = com.android.internal.R.style.Animation_Toast;
                params.type = WindowManager.LayoutParams.TYPE_TOAST;
                ...
            }
    
            /**
             * schedule handleShow into the right thread
             */
            @Override
            public void show() {
                if (localLOGV) Log.v(TAG, "SHOW: " + this);
                mHandler.post(mShow);
            }
           /**
             * schedule handleHide into the right thread
             */
            @Override
            public void hide() {
                if (localLOGV) Log.v(TAG, "HIDE: " + this);
                mHandler.post(mHide);
            }
            ....
     }
    

    可以看到TN是一个Binder对象,用来跨进程调用的,里面封装了show,hide方法,可以说,Toast的show,hide实际上就是调用了TN里面的方法。为什么这么说的,我们看下Toast的show方法。此外,注意WindowManager.LayoutParams.type,这里的Type是WindowManager.LayoutParams.TYPE_TOAST,表示系统窗口。

    Toast.show()

     public void show() {
            if (mNextView == null) {
                throw new RuntimeException("setView must have been called");
            }
    
            INotificationManager service = getService();
            String pkg = mContext.getOpPackageName();
            TN tn = mTN;
            tn.mNextView = mNextView;
    
            try {
                service.enqueueToast(pkg, tn, mDuration);
            } catch (RemoteException e) {
                // Empty
            }
        }
    

    Toast的show方法中,会先判断mNextView是否为空,这个mNextView是在Toast.makeText的时候创建赋值出来的。接着通过getService拿到NotificationManagerService的访问接口,接着把TN,包信息,以及设置时常发送到NotificationManagerService.enqueueToast中,我们看下这个方法

     @Override
            public void enqueueToast(String pkg, ITransientNotification callback, int duration)
            {
               ...
                synchronized (mToastQueue) {
                    ...
                    try {
                        ToastRecord record;
                        //从当前队列检查是否已经添加过了
                        int index = indexOfToastLocked(pkg, callback);
                        if (index >= 0) {
                            //添加过,则直接获取
                            record = mToastQueue.get(index);
                            record.update(duration);
                        } else {
                            ...
                            //否则重新创建个添加到队列
                            record = new ToastRecord(callingPid, pkg, callback, duration);
                            mToastQueue.add(record);
                            index = mToastQueue.size() - 1;
                            keepProcessAliveLocked(callingPid);
                        }
                        ...
                        if (index == 0) {
                            //开始显示
                            showNextToastLocked();
                        }
                    }
                   ...
                }
    

    可以看到,在enqueueToast中,首先会调用indexOfToastLocked来判断当前的TN也就是callback是否在队列中,如果有,则在mToastQueue中直接获取,更新时间。否则,则重新创建一个带有TN,时间,包信息的ToastRecord,再添加到队列。最后调用了showNextToastLocked显示

    void showNextToastLocked() {
            ToastRecord record = mToastQueue.get(0);
            while (record != null) {
                ...
                try {
                    record.callback.show();
                    scheduleTimeoutLocked(record);
                    return;
                } 
                ...
        }
    

    在showNextToastLocked中,先获取当前队列最前的ToastRecord,再调用recoed.callback.show(),这里的callback,就是前面我们传入的TN对象,也就是说调用到我们Toast中TN的show方法。

      @Override
            public void show() {
                if (localLOGV) Log.v(TAG, "SHOW: " + this);
                mHandler.post(mShow);
            }
    
      final Runnable mShow = new Runnable() {
                @Override
                public void run() {
                    handleShow();
                }
            };
    

    最终执行方法则是handleShow

     public void handleShow() {
                ...
                if (mView != mNextView) {
                    //移除之前的View
                    handleHide();
                    mView = mNextView;
                    ...
                    mWM =(WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                    ...
                    if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                }
            }
    

    在handleHide中,会先移除之前的View,然后把mNextView的值赋值给mView,前面也说到了,这个mNextView就是我们要显示的内容。接着获取WindowManager,调用addView请求WMS添加到窗口上,而因为是系统窗口,所以token为Null也是可以显示。

    执行完show方法后,Toast已经显示出来了,后面还调用了scheduleTimeoutLocked方法,没错,这个就是来限制显示时间的。

        private void scheduleTimeoutLocked(ToastRecord r)
        {
            mHandler.removeCallbacksAndMessages(r);
            Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
            long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
            mHandler.sendMessageDelayed(m, delay);
        }
    

    可以看到它把ToastRecode封装到Message,而你设置你时间则设置为Handler的delay时间,到达指定时间,则发送带Handler去,我们看下mHandler里面what为MESSAGE_TIMEOUT的执行情况。

            @Override
            public void handleMessage(Message msg)
            {
                switch (msg.what)
                {
                    case MESSAGE_TIMEOUT:
                        handleTimeout((ToastRecord)msg.obj);
                        break;
                    ...
                }
            }
    

    可以看到到达指定时间后调用了handleTimeout,接下来调用的方法肯定是来取消Toast显示的。

        private void handleTimeout(ToastRecord record)
        {
            if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
            synchronized (mToastQueue) {
                int index = indexOfToastLocked(record.pkg, record.callback);
                if (index >= 0) {
                    cancelToastLocked(index);
                }
            }
        }
    

    继续看

    void cancelToastLocked(int index) {
            ToastRecord record = mToastQueue.get(index);
            try {
                record.callback.hide();
            } 
            ...
            mToastQueue.remove(index);
            keepProcessAliveLocked(record.pid);
            if (mToastQueue.size() > 0) {
                // Show the next one. If the callback fails, this will remove
                // it from the list, so don't assume that the list hasn't changed
                // after this point.
                showNextToastLocked();
            }
        }
    

    可以看到,到达指定时间后,最终调用了TN的hide方法,然后移除队列,如果队列中还有其他的,则继续显示其他的。

    同理,看下hide方法

     @Override
            public void hide() {
                if (localLOGV) Log.v(TAG, "HIDE: " + this);
                mHandler.post(mHide);
            }
    
    
      final Runnable mHide = new Runnable() {
                @Override
                public void run() {
                    handleHide();
                    // Don't do this in handleHide() because it is also invoked by handleShow()
                    mNextView = null;
                }
            };
    
    
    
      public void handleHide() {
                if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
                if (mView != null) {
                    // note: checking parent() just to make sure the view has
                    // been added...  i have seen cases where we get here when
                    // the view isn't yet added, so let's try not to crash.
                    if (mView.getParent() != null) {
                        if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                        mWM.removeView(mView);
                    }
    
                    mView = null;
                }
            }
    

    可以看到hide方法最终调用了windowManager.removeView来取消显示。这也从另一方面看到看WindowManager的重要性。

    小结

    • WindowManager.LayoutParams中有三种类型,分别为

      • 应用程序窗口 : type值在 FIRST_APPLICATION_WINDOW ~ LAST_APPLICATION_WINDOW 须将token设置成Activity的token
        eg: 前面介绍的Activity窗口,Dialog

      • 子窗口: type值在 FIRST_SUB_WINDOW ~ LAST_SUB_WINDOW SubWindows与顶层窗口相关联,需将token设置成它所附着宿主窗口的token
        eg: PopupWindow(想要依附在Activity上需要将token设置成Activity的token)

      • 系统窗口: type值在 FIRST_SYSTEM_WINDOW ~ LAST_SYSTEM_WINDOW ** SystemWindows不能用于应用程序,使用时需要有特殊权限,它是特定的系统功能才能使用。
        eg: Toast,输入法等。

    • 对于Activity里面ActivityRecord的token,它间接标识了一个Activity。想要依附在Activity上需要将token设置成Activity的token,接着传到WMS中判断返回ViewRootImpl去判断报错。

    • View的绑定信息通过它的静态内部类AttachInfo在ViewRootImpl中绑定

    • Dialog中,与Activity共用同个WindowManager,但是他们两者的Window并不相同。可以说一个Window可以对应一个Activity,但一个Activity不一定对应一个Window,它也有可能对应Dialog

    五篇窗口机制总结

    • 了解掌握了Android中的窗口分类
    • 懂得Window,PhoneWindow,WindowManager,WindowManagerGlobal它们的分类区别作用
    • 掌握View的真正创建绘制
    • 熟悉了ViewRoot和View树机制
    • View的绑定信息,token


    作者:Hohohong
    链接:https://www.jianshu.com/p/bac61386d9bf
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    展开全文
  • NMS中会对Toast进行权限校验,当通知权限校验不通过时,Toast将不做展示。 当然不同ROM中NMS可能会有不同,比如MIUI就对这部分内容进行了修改,所以小米手机关闭通知权限不会导致Toast不显示。 /** * Show the view...

    Gradle引用

    Step 1. Add the JitPack repository to your build file

    Add it in your root build.gradle at the end of repositories:

    allprojects {

    repositories {

    ...

    maven { url 'https://jitpack.io' }

    }

    }

    Step 2. Add the dependency

    dependencies {

    implementation 'com.github.Dovar66:DToast:1.1.5'

    //implementation 'com.github.Dovar66:DToast:1.1.6'//for androidx

    }

    使用示例

    //使用默认布局

    DToast.make(mContext)

    .setText(R.id.tv_content_default, msg)

    .setGravity(Gravity.BOTTOM | Gravity.CENTER, 0, 30)

    .show();

    //通过setView()设置自定义的Toast布局

    DToast.make(mContext)

    .setView(View.inflate(mContext, R.layout.layout_toast_center, null))

    .setText(R.id.tv_content_custom, msg)

    .setGravity(Gravity.CENTER, 0, 0)

    .showLong();

    正文分析

    先看看使用系统Toast存在的问题:

    1.当通知权限被关闭时在华为等手机上Toast不显示;

    2.Toast的队列机制在不同手机上可能会不相同;

    3.Toast的BadTokenException问题;

    当发现系统Toast存在问题时,不少同学都会采用自定义的TYPE_TOAST弹窗来实现相同效果。虽然大部分情况下效果都是 OK的,但其实TYPE_TOAST弹窗依然存在兼容问题:

    4.Android8.0之后的token null is not valid问题;

    5.Android7.1之后,不允许同时展示两个TYPE_TOAST弹窗(实测部分机型问题)。

    那么,DToast使用的解决方案是:

    1.通知权限未被关闭时,使用SystemToast(修复了问题2和问题3的系统Toast);

    2.通知权限被关闭时,如果系统版本为Android8.0/8.1则通过hook绕过通知栏权限,否则使用DovaToast(自定义的TYPE_TOAST弹窗);

    3.当使用DovaToast出现token null is not valid时,尝试使用ActivityToast(自定义的TYPE_APPLICATION_ATTACHED_DIALOG

    弹窗,只有当传入Context为Activity时,才会启用ActivityToast).

    相信不少同学旧项目中封装的ToastUtil都是直接使用的ApplicationContext作为上下文,然后在需要弹窗的时候直接就是ToastUtil.show(str) ,这样的使用方式对于我们来说是最方便的啦。

    当然,使用DToast你也依然可以沿用这种封装方式,但这种方式在下面这个场景中可能会无法成功展示出弹窗(该场景下原生Toast也一样无法弹出), 不过请放心不会导致应用崩溃,而且这个场景出现的概率较小,有以下几个必要条件:

    1.你的应用设置的targetSdkVersion>=26.

    2.通知栏权限被关闭(通知栏权限默认都是打开的).

    3.非MIUI设备(MIUI弹吐司不需要通知栏权限).

    4.运行设备的系统版本在Android9.0及以上。

    所以,如果你的应用targetSdkVersion>=26,又想要保证在所有场景下都能正常展示弹窗,那么请在DToast.make(context)时传入Activity作为上下文,这样在该场景下DToast会启用ActivityToast展示出弹窗。而targetSdkVersion小于26的同学可以放心使用ApplicationContext创建DToast。

    想了解为什么需要区别对待targetSdkVersion26+?点击查看API26做了什么

    而如果你还不了解targetSdkVersion 点击这里查看

    接下来再详细分析下上面提到的五个问题:

    问题一:关闭通知权限时Toast不显示

    看下方Toast源码中的show()方法,通过AIDL获取到INotificationManager,并将接下来的显示流程控制权

    交给NotificationManagerService。

    NMS中会对Toast进行权限校验,当通知权限校验不通过时,Toast将不做展示。

    当然不同ROM中NMS可能会有不同,比如MIUI就对这部分内容进行了修改,所以小米手机关闭通知权限不会导致Toast不显示。

    /**

    * Show the view for the specified duration.

    */

    public void show() {

    if (mNextView == null) {

    throw new RuntimeException("setView must have been called");

    }

    INotificationManager service = getService();

    String pkg = mContext.getOpPackageName();

    TN tn = mTN;

    tn.mNextView = mNextView;

    try {

    service.enqueueToast(pkg, tn, mDuration);

    } catch (RemoteException e) {

    // Empty

    }

    }

    如何解决这个问题?只要能够绕过NotificationManagerService即可。

    DovaToast通过使用TYPE_TOAST实现全局弹窗功能,不使用系统Toast,也没有使用NMS服务,因此不受通知权限限制。

    问题二:系统Toast的队列机制在不同手机上可能会不相同

    我找了四台设备,创建两个Gravity不同的Toast并调用show()方法,结果出现了四种展示效果:

    * 荣耀5C-android7.0(只看到展示第一个Toast)

    * 小米8-MIUI10(只看到展示第二个Toast,即新的Toast.show会中止当前Toast的展示)

    * 红米6pro-MIUI9(两个Toast同时展示)

    * 荣耀5C-android6.0(第一个TOAST展示完成后,第二个才开始展示)

    造成这个问题的原因应该是各大厂商ROM中NMS维护Toast队列的逻辑有差异。 同样的,DToast内部也维护着自己的队列逻辑,保证在所有手机上使用DToast的效果相同。

    DToast中多个弹窗连续出现时:

    1.相同优先级时,会终止上一个,直接展示后一个;

    2.不同优先级时,如果后一个的优先级更高则会终止上一个,直接展示后一个。

    问题三:系统Toast的BadTokenException问题

    Toast有个内部类 TN(extends ITransientNotification.Stub),调用Toast.show()时会将TN传递给NMS; public void show() {

    if (mNextView == null) {

    throw new RuntimeException("setView must have been called");

    }

    INotificationManager service = getService();

    String pkg = mContext.getOpPackageName();

    TN tn = mTN;

    tn.mNextView = mNextView;

    try {

    service.enqueueToast(pkg, tn, mDuration);

    } catch (RemoteException e) {

    // Empty

    }

    }

    在NMS中会生成一个windowToken,并将windowToken给到WindowManagerService,WMS会暂时保存该token并用于之后的校验;

    NotificationManagerService.java #enqueueToast源码: synchronized (mToastQueue) {

    int callingPid = Binder.getCallingPid();

    long callingId = Binder.clearCallingIdentity();

    try {

    ToastRecord record;

    int index = indexOfToastLocked(pkg, callback);

    // If it's already in the queue, we update it in place, we don't

    // move it to the end of the queue.

    if (index >= 0) {

    record = mToastQueue.get(index);

    record.update(duration);

    } else {

    // Limit the number of toasts that any given package except the android

    // package can enqueue. Prevents DOS attacks and deals with leaks.

    if (!isSystemToast) {

    int count = 0;

    final int N = mToastQueue.size();

    for (int i=0; i

    final ToastRecord r = mToastQueue.get(i);

    if (r.pkg.equals(pkg)) {

    count++;

    if (count >= MAX_PACKAGE_NOTIFICATIONS) {

    Slog.e(TAG, "Package has already posted " + count

    + " toasts. Not showing more. Package=" + pkg);

    return;

    }

    }

    }

    }

    Binder token = new Binder();//生成一个token

    mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);

    record = new ToastRecord(callingPid, pkg, callback, duration, token);

    mToastQueue.add(record);

    index = mToastQueue.size() - 1;

    keepProcessAliveIfNeededLocked(callingPid);

    }

    // If it's at index 0, it's the current toast. It doesn't matter if it's

    // new or just been updated. Call back and tell it to show itself.

    // If the callback fails, this will remove it from the list, so don't

    // assume that it's valid after this.

    if (index == 0) {

    showNextToastLocked();

    }

    } finally {

    Binder.restoreCallingIdentity(callingId);

    }

    }

    然后NMS通过调用TN.show(windowToken)回传token给TN; /**

    * schedule handleShow into the right thread

    */

    @Override

    public void show(IBinder windowToken) {

    if (localLOGV) Log.v(TAG, "SHOW: " + this);

    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();

    }

    TN使用该token尝试向WindowManager中添加Toast视图(mParams.token = windowToken);

    在API25的源码中,Toast的WindowManager.LayoutParams参数新增了一个token属性,用于对添加的窗口进行校验。

    当param.token为空时,WindowManagerImpl会为其设置 DefaultToken; @Override

    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {

    applyDefaultToken(params);

    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);

    }

    private void applyDefaultToken(@NonNull ViewGroup.LayoutParams params) {

    // Only use the default token if we don't have a parent window.

    if (mDefaultToken != null && mParentWindow == null) {

    if (!(params instanceof WindowManager.LayoutParams)) {

    throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");

    }

    // Only use the default token if we don't already have a token.

    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;

    if (wparams.token == null) {

    wparams.token = mDefaultToken;

    }

    }

    }

    当WindowManager收到addView请求后会检查 mParams.token 是否有效,若有效则添加窗口展示,否则抛出BadTokenException异常. switch (res) {

    case WindowManagerGlobal.ADD_BAD_APP_TOKEN:

    case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:

    throw new WindowManager.BadTokenException(

    "Unable to add window -- token " + attrs.token

    + " is not valid; is your activity running?");

    case WindowManagerGlobal.ADD_NOT_APP_TOKEN:

    throw new WindowManager.BadTokenException(

    "Unable to add window -- token " + attrs.token

    + " is not for an application");

    case WindowManagerGlobal.ADD_APP_EXITING:

    throw new WindowManager.BadTokenException(

    "Unable to add window -- app for token " + attrs.token

    + " is exiting");

    case WindowManagerGlobal.ADD_DUPLICATE_ADD:

    throw new WindowManager.BadTokenException(

    "Unable to add window -- window " + mWindow

    + " has already been added");

    case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:

    // Silently ignore -- we would have just removed it

    // right away, anyway.

    return;

    case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:

    throw new WindowManager.BadTokenException("Unable to add window "

    + mWindow + " -- another window of type "

    + mWindowAttributes.type + " already exists");

    case WindowManagerGlobal.ADD_PERMISSION_DENIED:

    throw new WindowManager.BadTokenException("Unable to add window "

    + mWindow + " -- permission denied for window type "

    + mWindowAttributes.type);

    case WindowManagerGlobal.ADD_INVALID_DISPLAY:

    throw new WindowManager.InvalidDisplayException("Unable to add window "

    + mWindow + " -- the specified display can not be found");

    case WindowManagerGlobal.ADD_INVALID_TYPE:

    throw new WindowManager.InvalidDisplayException("Unable to add window "

    + mWindow + " -- the specified window type "

    + mWindowAttributes.type + " is not valid");

    }

    什么情况下windowToken会失效?

    UI线程发生阻塞,导致TN.show()没有及时执行,当NotificationManager的检测超时后便会删除WMS中的该token,即造成token失效。

    如何解决?

    Google在API26中修复了这个问题,即增加了try-catch:

    // Since the notification manager service cancels the token right

    // after it notifies us to cancel the toast there is an inherent

    // race and we may attempt to add a window after the token has been

    // invalidated. Let us hedge against that.

    try {

    mWM.addView(mView, mParams);

    trySendAccessibilityEvent();

    } catch (WindowManager.BadTokenException e) {

    /* ignore */

    }

    因此对于8.0之前的我们也需要做相同的处理。DToast是通过反射完成这个动作,具体看下方实现:

    //捕获8.0之前Toast的BadTokenException,Google在Android 8.0的代码提交中修复了这个问题

    private void hook(Toast toast) {

    try {

    Field sField_TN = Toast.class.getDeclaredField("mTN");

    sField_TN.setAccessible(true);

    Field sField_TN_Handler = sField_TN.getType().getDeclaredField("mHandler");

    sField_TN_Handler.setAccessible(true);

    Object tn = sField_TN.get(toast);

    Handler preHandler = (Handler) sField_TN_Handler.get(tn);

    sField_TN_Handler.set(tn, new SafelyHandlerWrapper(preHandler));

    } catch (Exception e) {

    e.printStackTrace();

    }

    }

    public class SafelyHandlerWrapper extends Handler {

    private Handler impl;

    public SafelyHandlerWrapper(Handler impl) {

    this.impl = impl;

    }

    @Override

    public void dispatchMessage(Message msg) {

    try {

    impl.dispatchMessage(msg);

    } catch (Exception e) {

    }

    }

    @Override

    public void handleMessage(Message msg) {

    impl.handleMessage(msg);//需要委托给原Handler执行

    }

    }

    问题四:Android8.0之后的token null is not valid问题

    Android8.0后对WindowManager做了限制和修改,特别是TYPE_TOAST类型的窗口,必须要传递一个token用于校验。

    API25:(PhoneWindowManager.java源码)

    public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {

    int type = attrs.type;

    outAppOp[0] = AppOpsManager.OP_NONE;

    if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)

    || (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)

    || (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {

    return WindowManagerGlobal.ADD_INVALID_TYPE;

    }

    if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {

    // Window manager will make sure these are okay.

    return WindowManagerGlobal.ADD_OKAY;

    }

    String permission = null;

    switch (type) {

    case TYPE_TOAST:

    // XXX right now the app process has complete control over

    // this... should introduce a token to let the system

    // monitor/control what they are doing.

    outAppOp[0] = AppOpsManager.OP_TOAST_WINDOW;

    break;

    case TYPE_DREAM:

    case TYPE_INPUT_METHOD:

    case TYPE_WALLPAPER:

    case TYPE_PRIVATE_PRESENTATION:

    case TYPE_VOICE_INTERACTION:

    case TYPE_ACCESSIBILITY_OVERLAY:

    case TYPE_QS_DIALOG:

    // The window manager will check these.

    break;

    case TYPE_PHONE:

    case TYPE_PRIORITY_PHONE:

    case TYPE_SYSTEM_ALERT:

    case TYPE_SYSTEM_ERROR:

    case TYPE_SYSTEM_OVERLAY:

    permission = android.Manifest.permission.SYSTEM_ALERT_WINDOW;

    outAppOp[0] = AppOpsManager.OP_SYSTEM_ALERT_WINDOW;

    break;

    default:

    permission = android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;

    }

    if (permission != null) {

    ...

    }

    return WindowManagerGlobal.ADD_OKAY;

    }

    API26:(PhoneWindowManager.java源码)

    public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {

    int type = attrs.type;

    outAppOp[0] = AppOpsManager.OP_NONE;

    if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)

    || (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)

    || (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {

    return WindowManagerGlobal.ADD_INVALID_TYPE;

    }

    if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {

    // Window manager will make sure these are okay.

    return ADD_OKAY;

    }

    if (!isSystemAlertWindowType(type)) {

    switch (type) {

    case TYPE_TOAST:

    // Only apps that target older than O SDK can add window without a token, after

    // that we require a token so apps cannot add toasts directly as the token is

    // added by the notification system.

    // Window manager does the checking for this.

    outAppOp[0] = OP_TOAST_WINDOW;

    return ADD_OKAY;

    case TYPE_DREAM:

    case TYPE_INPUT_METHOD:

    case TYPE_WALLPAPER:

    case TYPE_PRESENTATION:

    case TYPE_PRIVATE_PRESENTATION:

    case TYPE_VOICE_INTERACTION:

    case TYPE_ACCESSIBILITY_OVERLAY:

    case TYPE_QS_DIALOG:

    // The window manager will check these.

    return ADD_OKAY;

    }

    return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)

    == PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;

    }

    }

    为了解决问题一,DovaToast不得不选择绕过NotificationManagerService的控制,但由于windowToken是NMS生成的, 绕过NMS就无法获取到有效的windowToken,于是作为TYPE_TOAST的DovaToast就可能陷入第四个问题。因此,DToast选择在DovaToast出现 该问题时引入ActivityToast,在DovaToast无法正常展示时创建一个依附于Activity的弹窗展示出来,不过ActivityToast只会展示在当前Activity,不具有跨页面功能。 如果说有更好的方案,那肯定是去获取悬浮窗权限然后改用TYPE_PHONE等类型,但悬浮窗权限往往不容易获取,目前来看恐怕除了微信其他APP都不能保证拿得到用户的悬浮窗权限。

    问题五:Android7.1之后,不允许同时展示两个TYPE_TOAST弹窗

    DToast的弹窗策略就是同一时间最多只展示一个弹窗,逻辑上就避免了此问题。因此仅捕获该异常。

    其他建议

    新项目做应用架构的时候可以考虑把整个应用(除闪屏页等特殊界面外)做成只有一个Activity,其他全是Fragment,这样就不存在悬浮窗的问题啦。

    如果能够接受Toast不跨界面的话,建议使用SnackBar

    更新日志

    1.1.5

    新增IToast.setText(idRes,text)方法

    [修复]issue#6

    1.1.3

    [修复]issue#7

    1.1.2

    新增思路:对Toast的INotificationManager对象进行hook可以成功绕过通知栏权限,但9.0之后Android限制调用非公开API,所以9.0之后此方法不可用。

    代码更新:新增hook INotificationManager操作,在Android8.0/8.1上采用hook方式绕过通知栏权限。

    展开全文
  • 基础参数type- 基本层类型类型:Number,默认:0layer提供了5种层类型。可传入的值有:0(信息框,默认)1(页面层)2(iframe层)3(加载层)4(tips层)。 若你采用layer.open({type: 1})方式调用,则type为必填项(信息框...
  • 从网上找的方法如下 public class CustomToast { private static Toast mToast; private static Handler mHandler = new Handler(); private static Runnable r = new Runnable() { public void...
  • 本文记录两种情况下Android的toast无法显示的问题及提供对应的解决方案关闭通知权限在Android系统中将通知栏权限,我们可以发现toast无法展示出来(绝大部分机子,oppo和vivo一定系统及以上仍然可以正常显示,应该是...
  • 自定义Toast显示(不限时+在其他应用之上显示)一.首先写好自定义Toast的布局toast_view.xml android:layout_widt
  • 适配到Android 9,对于Android M以下的机型直接绕过悬浮窗权限弹出全局Toast 优化对context使用Application的支持,前提是需要提前在Application中注册EToastUtils 特别说明,本次更新的EToastUtils的实现方式参考...
  • Toast弹不出来之谜

    千次阅读 2018-12-14 12:18:34
    今天早上测试应用的时候,忽然发现Toast弹不出来了,我用的华为测试机,以为是通知权限被关了,后来发现是开着的,这就纳了闷了,这个Toast工具类用了好长时间了,后来发现这Toast原来还能这样... 正文 以前工具类是这个...
  • 解决方法:appium下切换selendroid模式去获取Android的toast提示。desired_caps配置:desired_caps_android={'platformVersion':'4.4',\'deviceName':'',\'udid':'34d7d220',\'platformName':'android',\'...
  • 修改toast样式

    千次阅读 2019-04-19 09:50:24
    //获取窗体对象 private WindowManager mWM; private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams... //在窗体上挂在一个view(权限) mWM.addView(mViewToast, params); }  
  • 华为、三星等机型禁用通知权限Toast不弹出原因查看Toast源码后发现,Toast显示要通过INotificationManager类来实现,而当通知禁用后,调用此类会返回异常,所以导致通知不显示,源码如下:public void show() {if ...
  • 前言Android无需权限显示悬浮窗, 兼谈逆向分析app这篇文章阅读量很大, 这篇文章是通过逆向分析UC浏览器的实现和兼容性处理来得到一个悬浮窗的实现小技巧, 但有很多问题没有弄明白, 比如为什么在API 18及以下TYPE_...
  • 效果图参考问题分析Android 5.0以上系统将消息通知默认为关闭,而Toast源码中有如下一段代码:/*** Show the view for the specified duration.*/public void show() {if (mNextView == null) {throw new ...
  • 如果发现手机弹不出Toast

    千次阅读 2019-06-17 15:15:05
    错误情景 ...第一步:看下他的设备是什么手机,对应机型搜索下通知权限的设置,或者"xxx 弹不出 Toast" 例如: 华为手机:打开设置页面,找到通知管理,找到找到对应的应用,允许通知。解决。 ...
  • 1、现象目前测试中发现的现象有两种:在使用Type Toast悬浮窗的同时,使用了Toast,必现崩溃,即使catch 了Throwable也无法解决,但是将悬浮窗addView的代码注释之后则不出现崩溃;如果只有Type Toast悬浮窗,不会...
  • python+appium toast获取

    2021-03-27 22:11:04
    在app自动化测试的过程中经常会遇到需要对toast进行定位,最常见的就是定位toast或者获取toast的文案进行断言,如下图,通过定位"登录成功"的toast就可以断言今日头条登录用例是否通过。但toast区别于控件元素,无法...
  • Toast 判断-----基本操作问题首先基本操作,进入安卓市场的账号密码页面---from appium importwebdriverfrom selenium.webdriver.support.ui importWebDriverWaitfrom selenium.webdriver.support importexpected_...
  • Android中的悬浮窗显示是一个非常棘手的问题,网上已经有很多解决方案了,大致归为下面两类:设置WindowManager.LayoutParams.type = TYPE_SYSTEM_ALERT,并引导用户打开悬浮窗权限。这种方法主要的难点在于引导用户...
  • DevUtilsAndroid开发工具类,常用的文件操作,bitmap操作,数据库操作,Toast显示,Dialog,反射、大图查看、权限管理等如何使用:implementation 'com.jzw.dev:devutils:3.3'Androidx支持implementation '...
  • PermissionUtil经常写Android运行时权限申请代码,每次都是复制过来之后,改一下权限字符串就用,把代码搞得乱糟糟的,于是便有了封装工具类的想法,话不多说,先看怎么用:工具类及Demo:github简洁版申请权限申请...
  • android权限处理详解

    千次阅读 2021-06-04 04:25:19
    写在前面对于android 6.0来说,增加了权限的管理...android权限分类Android6.0系统把权限分为两个级别:Normal Permissions,即普通权限,这类权限不会潜藏有侵害用户隐私和安全的问题,比如,访问网络的权限,访问...
  • 解决方法:appium下切换selendroid模式去获取Android的toast提示。 desired_caps配置: desired_caps={'platformVersion':'4.4',\ 'deviceName':'android',\ 'udid':'34d7d220',\ 'platformName':'...
  • App的界面消息提示中,Toast和Snackbar是咱们经常打交道的哥俩,在使用的过程中,如果不加以封装和处理,调用的简易性以及性能和用户体验上就会存在诸多问题。下面给大家介绍一个我封装的库,SmartShow的...
  • Android WindowManager悬浮窗:不需要申请权限实现悬浮附录文章1介绍了Android平台上的悬浮窗WindowManager,WindowManager悬浮窗可以悬浮在Android设备上的桌面窗口之上,但是WindowManager的使用,必须先申请...
  • 引言:去年Android6.0发布后,其新引入的(Requesting Permissions at Run Time)运行时权限就备受开发者关注,随着今年国内手机厂商对6.0系统的普及,觉得大家有必要了解下这个新特性,因为在TargetSDK23+进行开发不...
  • 自定义Toast显示归属地
  • 在android5.0是正常可以判断,并且弹出Toast。但是android6.0的机子进行运行,不能弹出Toast。网上搜索之后,有人说这是因为Toast不能用全局变量Context…还是迷茫不知道到底是怎么回事。望解答!//检查网络方法,...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 47,486
精华内容 18,994
关键字:

toast权限