安卓视图层级大揭秘
前言
最近接了一个语音控制的功能,UI上的具体实现就是在应用上遮盖一个透明防触层,在语音状态下阻止用户点击,但不能影响物理返回键的Dialog呼出即控制,同时对于非物理返回键呼出的Dialog也要阻止操作。功能看起来很绕,我们用一张图片来具体说明一下。
语音控制功能需求分析.png
通过图片不难看出,我们要实现的语音控制层其实是介于应用视图与视图内部提示框之上,同时又在Back返回键弹窗之下的一个层级。因为一直以来对安卓视图层级的探究不是很深入,所以借着做这个功能对安卓视图层级这一块的知识进行了一下总结梳理。
视图层级初探
首先让我们通过一张层级图来明确几个重要的概念Window,DecorView和mContentParent。
安卓层级图.png
Window
在Android中不管是Activity、Toast、ActionBar还是Dialog,他们的视图都是附加到Window上,其实基本上所有的view同时通过Window来呈现的,因此Window可以理解为是view的承载者和管理者。Window 有三种类型,分别是应用 Window、子 Window 和系统 Window。应用类 Window 对应一个 Acitivity,子 Window 不能单独存在,需要依附在特定的父 Window 中,比如常见的一些 Dialog 就是一个子 Window。系统 Window是需要声明权限才能创建的 Window,比如 Toast 和系统状态栏都是系统 Window。
DecorView
DecorView是Windows中的View的最顶层View。其实DecorView是FrameLayout的子类,它里面包含了一个存有ActionBar以及mContentParent的LinearLayout。
mContentParent
mContentParent这个名字可能会有些陌生,其实他就是我们经常使用的应用根布局,即android.R.id.content。Activity中的setContentView其实就是通过LayoutInflater将XML布局转换成View并添加到mContentParent中。
源码分析
每个Activity都会持有一个Window,而在安卓中,Window只有唯一的一个实现类PhoneWindow ,所以每个Activity都会持有一个PhoneWindow,在PhoneWindow中会持有顶层视图DecorView。那么Activity是怎么建立与PhoneWindow的联系的呢,让我们通过源码来探究一下:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
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 = PolicyManager.makeNewWindow(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
......
mWindowManager = mWindow.getWindowManager();
......
}
在Activity的启动过程中会执行ActivityThread的performLaunchActivity方法,其中调用Activity的attach。在attach()方法中实例化Activity持有的mWindow。由于 Activity 实现了 Window 的 Callback 接口,因此当 Window 接受到外界的状态改变时就会回调 Activity 的方法。
public class PhoneWindow extends Window implements MenuBuilder.Callback {
private final static String TAG = "PhoneWindow";
...
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
private ViewGroup mContentParent;
private ViewGroup mContentRoot;
...
}
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
/* package */int mDefaultOpacity = PixelFormat.OPAQUE;
/** The feature ID of the panel, or -1 if this is the application's DecorView */
private final int mFeatureId;
private final Rect mDrawingBounds = new Rect();
....
}
可以看到,在PhoneWindow里面,出现了成员变量DecorView。而这里,DecorView则是PhoneWindow里面的一个内部类,它是继承于FrameLayout。
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public Window getWindow() {
return mWindow;
}
这是我们每次写Activity都会调用的setContentView方法,它的内部调用了getWindow()的setContentView,这个mWindow就是PhoneWindow。
public class PhoneWindow extends Window implements MenuBuilder.Callback {
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mContentParent.addView(view, params);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
}
我们看到在PhoneWindow中有三个setContentView的重载方法。在setContentView(int layoutResID)中,首先判断了mContentParent ,如果mContentParent 为空即为第一次调用的时候,就执行installDecor()方法,创建DecorView,并添加到mContentParent上。如果mContentParent不为空,那么将mContentParent中的view移除。接着通过mLayoutInflater将XML转换为View树,并且添加至mContentParent视图中。 添加完成后回调通知onContentChanged,表示完成界面加载。
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
//第一次DecorView未加载到mContentParent,所以此时mContentParent=null
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
}
}
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
首先判断mDecor是否为空,如果为空则通过generateDecor创建一个DecorView,紧接着设置DecorView的获取焦点能力为FOCUS_AFTER_DESCENDANTS,即先分发给Child View进行处理,如果所有的Child View都沒有处理,则自己再处理。第一次DecorView未加载到mContentParent,所以mContentParent为空,调用generateLayout将setContentView内容添加到mContentParent。
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
//根据当前设置的主题来加载默认布局
TypedArray a = getWindowStyle();
//判断style是否设置了no_title或是no_action_bar
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
//判断style是否设置了全屏显示
if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
}
mDecor.startChanging();
layoutResource = R.layout.screen_simple;
//选择对应布局创建添加到DecorView中
View in = mLayoutInflater.inflate(layoutResource, null);
//设置宽高
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
//返回id为android.R.id.content的viewGroup
return contentParent;
}
定制过Acitivity的Actionbar或是Fullscreen的同学一定都知道,requesetFeature方法需要在setContentView之前调用,这就是原因。setContentView的实质显示是触发了Activity的resume状态,也就是触发了makeVisible方法。
void makeVisible() {
if (!mWindowAdded) {
//也就是获取Activity的mWindowManager
//这个mWindowManager是在Activity的attach中通过mWindow.getWindowManager()获得
ViewManager wm = getWindowManager();
//调运的实质就是ViewManager接口的addView方法,传入的是mDecorView
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
这里将getWindow().getAttributes()作为了LayoutParams,在WindowManager中:
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;
format = PixelFormat.OPAQUE;
}
可以看到Activity的窗口类型是TYPE_APPLICATION,这个TYPE类型决定了在Window层的显示层级,TYPE类型总览如下:
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) {
......
}
......
}
Dialog不属于View,他是应用的子window,所以这也是为什么我们通过给mContentParent添加view无法实现遮挡Dialog的原因。Dialog 中 Window 同样是通过 PolicyManager 的 makeNewWindow 方法来完成的,普通的 Dialog 必须采用 Activity 的 Context,如果采用 Application 的 Context 就会报错。这是因为没有应用 token 导致的,而应用 token 一般只有 Activity 拥有。常规Dialog的TYPE为TYPE_APPLICATION_ATTACHED_DIALOG,通过不同的TYPE层级划分我们可以找到置于常规Dialog之上的WindowManager LayoutParams 属性,例如TYPE_SYSTEM_ALERT与TYPE_TOAST,设置了这两个属性的布局是可以将常规Dialog完全遮盖的。他们的区别在于一个是系统级别的Dialog一个是Toast,系统Dialog需要申请权限。所以我们的第一个方案就是可遮挡的Dialog使用常规Dialog,语音提示框采用TYPE_SYSTEM_ALERT。但是都知道安卓有一个无法逃避的问题,就是厂商定制,在MUI的framework层,出于对“安全”的考虑,默认为用户关闭了悬浮窗权限,也就是是说设置了TYPE_SYSTEM_ALERT属性的视图默认是无法显示的,需要用户手动开启权限以后方可显示。
虽然可以在用户启动的时候根据用户机型选择跳转开启权限页,但作为一个有情怀的开发这种不完美的体验还是不能接受的。根据之前对安卓视图层级的学习,我们有了第二套方案。应用视图是存放于mContentParent他与Activity同属TYPE_APPLICATION Window层级属于最下层,常规Dialog的层级是TYPE_APPLICATION_ATTACHED_DIALOG,所以我们将常规Dialog作为最上层不可遮挡的提示框,下面只需考虑可遮挡的弹窗与语音控制两层即可。因为语音控制层需要能够遮挡提示弹窗,所以需要语音控制层在弹窗的上层,经过之前的学习,我们把弹窗加入到mContentParent,把语音控制层添加到DecorView层即可完美的解决问题。mContentParent为一个FrameLayout,应用视图通过sentContentView率先添加到mContentParent中,作为提示弹窗,添加顺序一定相对应用视图置后,所以当提示弹窗再次向mContentParent添加的时候,即会添加到应用视图之上。而DecorView是mContentParent的父容器,也是一个FrameLayout,添加语音提示框的时候mContentParent一定已经存在,所以添加的时候一定会在mContentParent之上。
后记
就这样,一个看似复杂的需求通过对安卓源码的探究完美的解决了,很多时候当我们遇到难以解决的问题,不妨试试回到问题的原点,思考一下问题的本质,很多时候都会有不一样的发现。