本篇基于Android Q代码
根据AppWindowToken和WindowToken的添加流程和排序规则我们知道Android细分了四大窗口容器,分别是存储输入法相关的mImeWindowsContainers,存储系统窗口的mAboveAppWindowsContainers,存储应用窗口的TaskStackContainers,存储壁纸窗口的mBelowAppWindowsContainers,输入法和壁纸为何特殊,我们接下来将对输入法窗口的添加策略进行分析,

输入法类型窗口在向WMS添加之前必须显式创建自己的WindowToken,并提前添加到mImeWindowsContainers中,Android系统在开机时会启动输入法服务(InputMethodManagerService),在InputMethodManagerService的systemRunning方法中调用startInputInnerLocked方法,此方法中会给输入法创建
token,我们可以看到,其实token就是一个简单的Binder对象,然后在WMS addWindowToken方法会使用token创建WindowToken,在WindowToken构造方法中最终调到DisplayContent的addWindowToken方法,将输入法对应的WindowToken添加到mImeWindowsContainers中,关于WindowToken相关添加流程在AppWindowToken和WindowToken的添加流程和排序规则有非常详细的分析

InputBindResult startInputInnerLocked() {......mCurToken = new Binder();try {if (DEBUG) Slog.v(TAG, "Adding window token: " + mCurToken);mIWindowManager.addWindowToken(mCurToken, TYPE_INPUT_METHOD, DEFAULT_DISPLAY);} catch (RemoteException e) {}......return null;}

输入法这种窗口非常特殊,它永远会在需要使用输入法的窗口的上面,感觉像个子窗口,并且输入法窗口一旦第一次添加之后一般不会销毁,当我们某个应用需要使用输入法时不需要重新创建窗口,只要计算出是哪个应用,然后移动输入法窗口的堆栈位置到此窗口上面
我们具体来看看WMS是如何完成对输入法的添加策略的,输入法窗口添加有两种情况:
一是输入法窗口第一次通过WMS.addWindow方法添加到目标窗口时需要找到满足输入法添加条件的目标窗口
二是输入法窗口已经创建之后,系统Window位置调整即调用WMS的relayoutWindow方法之后,输入法位置也会调整

先看一的情况:
WMS.addWindow

public int addWindow(Session session, IWindow client, int seq,WindowManager.LayoutParams attrs, int viewVisibility, int displayId,Rect outContentInsets, Rect outStableInsets, Rect outOutsets,InputChannel outInputChannel) {......//输入法必须提前添加WindowToken的检查就在这里WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);final int rootType = hasParent ? parentWindow.mAttrs.type : type;//如果token为空并且是输入法窗口是不允许的if (token == null) {......if (rootType == TYPE_INPUT_METHOD) {return WindowManagerGlobal.ADD_BAD_APP_TOKEN;}//创建输入法的WindowStatefinal WindowState win = new WindowState(this, session, client, token, parentWindow,appOp[0], seq, attrs, viewVisibility, session.mUid,session.mCanAddInternalSystemWindow);......//是否需要移动输入法窗口的位置,默认需要boolean imMayMove = true;win.mToken.addWindow(win);//如果是输入法窗口if (type == TYPE_INPUT_METHOD) {displayContent.setInputMethodWindowLocked(win);imMayMove = false;//如果是输入法对话框} else if (type == TYPE_INPUT_METHOD_DIALOG) {displayContent.computeImeTarget(true /* updateImeTarget */);imMayMove = false;}}}

要注意输入法窗口的WindowToken是在系统启动的时候添加的,但是WindowState是在输入法窗口第一次需要添加的时候才创建的,所以在刚开机在Launcher界面dump窗口信息会发现mImeWindowsContainers 只有WindowToken,而WindowToken下并没有WindowState

ROOT type=undefined mode=fullscreen#0 Display 0 name="Built-in screen" type=undefined mode=fullscreen#3 mImeWindowsContainers type=undefined mode=fullscreen#0 WindowToken{f127d64 android.os.Binder@d21eaf7} type=undefined mode=fullscreen

displayContent.setInputMethodWindowLocked

 void setInputMethodWindowLocked(WindowState win) {mInputMethodWindow = win;// Update display configuration for IME process.if (mInputMethodWindow != null) {final int imePid = mInputMethodWindow.mSession.mPid;mWmService.mAtmInternal.onImeWindowSetOnDisplay(imePid,mInputMethodWindow.getDisplayId());}//计算可以显示输入法窗口的目标窗口,true代表需要更新输入法窗口computeImeTarget(true /* updateImeTarget */);mInsetsStateController.getSourceProvider(TYPE_IME).setWindow(win,null /* frameProvider */);}

computeImeTarget

这个方法会被多次调用以精确计算出可以添加输入法的目标窗口

WindowState computeImeTarget(boolean updateImeTarget) {//mInputMethodWindow不为空if (mInputMethodWindow == null) {if (updateImeTarget) {if (DEBUG_INPUT_METHOD) Slog.w(TAG_WM, "Moving IM target from "+ mInputMethodTarget + " to null since mInputMethodWindow is null");setInputMethodTarget(null, mInputMethodTargetWaitingAnim);}return null;}//mInputMethodTarget等于当前输入法窗口所在的窗口final WindowState curTarget = mInputMethodTarget;//这个方法就是对mDeferUpdateImeTargetCount==0的判断//mDeferUpdateImeTargetCount在调用deferUpdateImeTarget方法//后自增1,看注释是延迟显示输入法的作用if (!canUpdateImeTarget()) {return curTarget;}mUpdateImeTarget = updateImeTarget;//寻找可以添加输入法的目标窗口,通过mComputeImeTargetPredicate规则//mComputeImeTargetPredicate是一个java8新增的Predicate类型WindowState target = getWindow(mComputeImeTargetPredicate);//后面代码等下分析的时候再贴出来.....}

我们先来看下输入法的目标窗口的查找规则 ,getWindow是DisplayContent父类WindowContainer的方法,DisplayContent并没有覆盖

WindowState getWindow(Predicate<WindowState> callback) {for (int i = mChildren.size() - 1; i >= 0; --i) {final WindowState w = mChildren.get(i).getWindow(callback);if (w != null) {return w;}}return null;}

我们看下这个方法,遍历mChildren,每个元素都调用getWindow方法,并且注意它是从mChildren尾部窗口开始的,什么意思呢?
在AppWindowToken和WindowToken的添加流程和排序规则我们分析过,DisplayContent的mChildren里放的是四大窗口容器,这四大容器添加规则就是越尾部的窗口Z-order越高,说明我们只需要找到Z-order最高的那个满足添加输入法条件的窗口就行了,因为输入法窗口总是给用户使用的,肯定需要出现在当前层级最高的窗口上啊,接着这四大窗口容器也会遍历自己内部的mChildren,再调用getWindow方法,这样一层一层的调用最终会调用到WindowState的getWindow,而且是系统所有WindowState都会调用,这样就实现了对系统所有窗口的遍历查找,非常的巧妙,而且我们可以去看源码,WindowContainer的所有子类,只有WindowState的对getWindow方法有具体实现。
getWindow方法参数接收的是一个Predicate,这是java8新增的类,类似断言assert的作用,将Predicate传递给每个WindowState,Predicate的test方法会将传入test的参数传递到定义的Predicate表达式中,即看WindowState是否满足canBeImeTarget的条件

 private final Predicate<WindowState> mComputeImeTargetPredicate = w -> {return w.canBeImeTarget();};

看下WindowState的getWindow方法具体实现,test方法为true则代表找到了canBeImeTarget为true的窗口,此窗口即是能够显示输入法的窗口,则返回此窗口

WindowState getWindow(Predicate<WindowState> callback) {//WindowState的mChildren存储的是子窗口的WindowState//为空代表没有子窗口,则直接对当前窗口进行test判断if (mChildren.isEmpty()) {return callback.test(this) ? this : null;}int i = mChildren.size() - 1;WindowState child = mChildren.get(i);//对mSubLayer大于0的子窗口进行遍历,寻找满足canBeImeTarget的//目标子窗口while (i >= 0 && child.mSubLayer >= 0) {if (callback.test(child)) {return child;}--i;if (i < 0) {break;}child = mChildren.get(i);}//有子窗口的情况,对当前父窗口进行test判断if (callback.test(this)) {return this;}//如果mSubLayer大于0的子窗口和父窗口都不满足test条件则就对//所有子窗口进行判断,包括mSubLayer小于0的while (i >= 0) {if (callback.test(child)) {return child;}--i;if (i < 0) {break;}child = mChildren.get(i);}return null;}

继续看WindowState的canBeImeTarget方法

boolean canBeImeTarget() {//如果调用test方法的窗口也是输入法窗口直接返回false,不允许的if (mIsImWindow) {return false;}//如果当前窗口是应用类型窗口并且没有获得焦点则windowsAreFocusable为false//否则为true,系统不会让应用在没有焦点时弹输入法,这很危险//如果当前窗口是系统窗口则不管是否获得焦点windowsAreFocusable都为truefinal boolean windowsAreFocusable = mAppToken == null || mAppToken.windowsAreFocusable();if (!windowsAreFocusable) {return false;}//FLAG_NOT_FOCUSABLE和FLAG_ALT_FOCUSABLE_IM按位或之后//再和当前窗口的flags进行按位与final int fl = mAttrs.flags & (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM);final int type = mAttrs.type;//fl != (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM)这段逻辑我//没怎么看明白就不解释了,总之满足这三个条件则该窗口不会作为输入法//目标窗口if (fl != 0 && fl != (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM)&& type != TYPE_APPLICATION_STARTING) {return false;}//通过窗口是否可见或者是否添加判断是否可以弹出输入法return isVisibleOrAdding();}

WindowState.isVisibleOrAdding

这几个条件是:(存在Surface ||(没有调用relayoutWindow重新更新窗口布局 && 当前窗口是可见的)) && 确保设置了所有策略可见性位 && 父窗口没有隐藏 && (AppWindowToken不为空时需要确保该应用没被隐藏) && 没有执行窗口退出动画 && 没被销毁,其实这些判断都是确认当前窗口是否可见,如果是应用类型窗口还必须要求获取了焦点,这点是在上面canBeImeTarget里判断的

    boolean isVisibleOrAdding() {final AppWindowToken atoken = mAppToken;return (mHasSurface || (!mRelayoutCalled && mViewVisibility == View.VISIBLE))&& isVisibleByPolicy() && !isParentWindowHidden()&& (atoken == null || !atoken.hiddenRequested)&& !mAnimatingExit && !mDestroying;

通过getWindow方法最终就获取到了Z-order最高的并且可见的窗口,如果是应用类型窗口则获取到的是Z-order最高并且可见并且有焦点

再回到computeImeTarget方法接着往下看

WindowState computeImeTarget(boolean updateImeTarget) {//mInputMethodWindow不为空if (mInputMethodWindow == null) {if (updateImeTarget) {setInputMethodTarget(null, mInputMethodTargetWaitingAnim);}return null;}//mInputMethodTarget等于当前输入法窗口所在的窗口final WindowState curTarget = mInputMethodTarget;//这个方法就是对mDeferUpdateImeTargetCount==0的判断//mDeferUpdateImeTargetCount在调用deferUpdateImeTarget方法//后自增1,看注释是延迟显示输入法的作用if (!canUpdateImeTarget()) {return curTarget;}mUpdateImeTarget = updateImeTarget;//寻找可以添加输入法的目标窗口,通过mComputeImeTargetPredicate规则//mComputeImeTargetPredicate是一个java8新增的Predicate类型WindowState target = getWindow(mComputeImeTargetPredicate);//如果获取到了可以添加输入法的窗口并且窗口类型是启动窗口if (target != null && target.mAttrs.type == TYPE_APPLICATION_STARTING) {final AppWindowToken token = target.mAppToken;if (token != null) {//getImeTargetBelowWindow方法作用是判断如果当前启动窗口的//AppWindowToken下有除了此启动窗口外的其他应用窗口,则将输入法目标//窗口赋值给启动窗口前一个满足canBeImeTarget的窗口final WindowState betterTarget = token.getImeTargetBelowWindow(target);if (betterTarget != null) {target = betterTarget;}}}//如果当前输入法的目标窗口不为空,并且还没被移除但是正在被移除的过程//中,则不会给输入法寻找新窗口if (curTarget != null && !curTarget.mRemoved && curTarget.isDisplayedLw() && curTarget.isClosing()) {return curTarget;}//没找到可以添加输入法的目标窗口if (target == null) {//设置输入法目标窗口为空setInputMethodTarget(null, mInputMethodTargetWaitingAnim);}return null;}if (updateImeTarget) {AppWindowToken token = curTarget == null ? null : curTarget.mAppToken;//如果当前输入法所在的目标窗口不为空,且为应用类型窗口if (token != null) {WindowState highestTarget = null;//当前应用窗口正在等待执行动画或者正在执行动画if (token.isSelfAnimating()) {//getHighestAnimLayerWindow的作用是从这个isSelfAnimating//的窗口开始遍历之前的所有窗口,找到第一个mRemoved为false的窗口//作为输入法新的目标窗口highestTarget = token.getHighestAnimLayerWindow(curTarget);}//如果能找到这么一个新窗口存在if (highestTarget != null) {//此窗口设置了切换动画,正准备执行if (mAppTransition.isTransitionSet()) {//将此窗口设置为输入法目标窗口,并且mInputMethodTargetWaitingAnim//等于true,代表该窗口正在准备执行切换动画//则推迟输入法窗口的添加setInputMethodTarget(highestTarget, true);return highestTarget;}}}//上面的特殊条件都没满足则给输入法设置目标窗口,并且//mInputMethodTargetWaitingAnim为falsesetInputMethodTarget(target, false);}return target;}

computeImeTarget这个方法还是比较复杂的,主要是计算输入法窗口的目标窗口,总结一下:

  1. 通过DisplayContent的getWindow方法会遍历当前系统所有WindowState,都执行getWindow方法,目的是找出输入法的目标窗口,并且遍历的时候是从Z-order最高的窗口开始,一旦找到就不会在遍历了,判断输入法目标窗口的条件是调用WindowState的canBeImeTarget方法

  2. 通过getWindow获取到了输入法目标窗口之后,会对此窗口进行一些特殊性判断,(1)如果此窗口是应用启动窗口,则从此应用启动窗口开始遍历它之前的应用窗口,寻找到第一个满足canBeImeTarget的窗口作为输入法目标的新窗口,(2)如果当前输入法所在的窗口curTarget不为空,并且还没被移除但是正在被移除的过程中并且是可见的,则不会继续给输入法寻找新窗口,(3)如果找到的输入法目标窗口是应用窗口,并且正在等待执行切换动画或者正在执行动画,则此窗口不能再作为输入法目标窗口,而是调用getHighestAnimLayerWindow方法寻找新的目标窗口,规则不重复了,代码中写了注释,并且这个新的目标窗口已经设置了动画准备切换则通过setInputMethodTarget方法真正将此窗口设置为输入法目标窗口,并将mInputMethodTargetWaitingAnim赋值为true

  3. 如果通过getWindow方法找到的输入法目标窗口不是2描述的特殊窗口则调用setInputMethodTarget方法真正给输入法设置目标窗口,mInputMethodTargetWaitingAnim为false

setInputMethodTarget

输入法的目标窗口已经找到,接着调用setInputMethodTarget方法将输入法设置给目标窗口

private void setInputMethodTarget(WindowState target, boolean targetWaitingAnim) {//如果当前要给输入法设置的目标窗口就是上一个输入法窗口,并且//mInputMethodTargetWaitingAnim也没有变化则直接returnif (target == mInputMethodTarget && mInputMethodTargetWaitingAnim == targetWaitingAnim) {return;}mInputMethodTarget = target;mInputMethodTargetWaitingAnim = targetWaitingAnim;//setLayoutNeeded为falseassignWindowLayers(false /* setLayoutNeeded */);mInsetsStateController.onImeTargetChanged(target);updateImeParent();}

assignWindowLayers

void assignWindowLayers(boolean setLayoutNeeded) {Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "assignWindowLayers");//getPendingTransaction()处理动画相关assignChildLayers(getPendingTransaction());if (setLayoutNeeded) {setLayoutNeeded();}//准备开始一个动画scheduleAnimation();Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);}

assignChildLayers

@Overridevoid assignChildLayers(SurfaceControl.Transaction t) {//设置三大窗口容器的Layer为0,1,2mBelowAppWindowsContainers.assignLayer(t, 0);mTaskStackContainers.assignLayer(t, 1);mAboveAppWindowsContainers.assignLayer(t, 2);final WindowState imeTarget = mInputMethodTarget;boolean needAssignIme = true;//如果输入法目标窗口不为空并且非(分屏模式或者imeTarget没有在执行动画)//并且imeTarget的SurfaceControl不为空if (imeTarget != null && !(imeTarget.inSplitScreenWindowingMode()|| imeTarget.mToken.isAppAnimating())&& (imeTarget.getSurfaceControl() != null)) {//则对输入法系列窗口容器执行assignRelativeLayer,这个方法//就是将输入法窗口层级调整到输入法目标窗口之上,这里调整的是//SurfaceFlinger里面的Layer层级mImeWindowsContainers.assignRelativeLayer(t, imeTarget.getSurfaceControl(),1);//不需要再对输入法进行调整needAssignIme = false;}//下面这四个容器都分别调用assignChildLayers方法,对自己内部所有窗口//都会调整Layer,这里的Layer和Z-order是对应的,Layer最终是设置到//SurfaceFlinger中去,就不去细看了mBelowAppWindowsContainers.assignChildLayers(t);mTaskStackContainers.assignChildLayers(t);mAboveAppWindowsContainers.assignChildLayers(t,needAssignIme == true ? mImeWindowsContainers : null);mImeWindowsContainers.assignChildLayers(t);}

到这里我们对输入法窗口向目标窗口的添加已经分析的差不多了,核心方法就是computeImeTarget,思路其实就是输入法窗口根据一定规则找到目标窗口,接着将输入法窗口容器assign到目标窗口的上面,上面分析的添加输入法窗口的情况是直接通过WMS.addWindow添加一个类型为TYPE_INPUT_METHOD的窗口,还有可能会添加一个不是输入法类型窗口,并且此窗口可以接收事件canReceiveKeys方法返回true,可以接收事件则焦点就可能发生变化,焦点变化之后输入法窗口就会重新调整位置,重新计算输入法目标窗口,源码就不详细分析了,贴一个调用流程:

WMS.addWindow -> WMS.updateFocusedWindowLocked -> RootWindowContainer.updateFocusedWindowLocked -> DisplayContent.updateFocusedWindowLocked -> DisplayContent.computeImeTarget

最终调用computeImeTarget方法重新计算输入法窗口的目标窗口

其实还有一种情况也会引起输入法目标窗口的重新计算,当WMS调用relayoutWindow对窗口重新布局的时候,但是最终都是调用的computeImeTarget来计算目标窗口,只是调用条件不同而已,我们就不再进行分析了。

Android特殊窗口之输入法窗口的添加策略相关推荐

  1. Android系统内置第三方输入法

    Android系统内置第三方输入法 一.添加APK进系统目录 1.1  package/目录下创建子目录,例如:package/inputmethod/SogouInput 1.2 将下载好的输入法A ...

  2. android 输入法 悬浮窗口,Android EditText悬浮在输入法之上

    Android EditText悬浮在输入法之上 使用 android:windowSoftInputMode="adjustResize" 会让界面整体被顶上去,很多时候我们不需 ...

  3. Android WindowManagerService机制分析:窗口的显示层级

    WindowManagerService(以下简称WMS)是Android Framework中一个重要的系统服务,用来管理系统中窗口(Window)的行为.Window是一个抽象的概念,它是一个矩形 ...

  4. Android应用小工具(窗口小部件)

    Widget是可以在其他应用程序被嵌入和接收定期更新的微型应用程序视图. 在创建一个应用程序窗口小部件,需要满足以下条件: AppWidgetProviderInfo--描述元数据为应用窗口小部件,如 ...

  5. android中弹出窗口,如何在Android中创建弹出窗口(PopupWindow)

    如何制作一个简单的Android弹出窗口 这是一个更完整的例子.这是一个补充性答案,涉及一般情况下创建弹出窗口的过程,而不一定是OP问题的具体细节.(OP要求取消按钮,但这不是必需的,因为用户可以在屏 ...

  6. Android 浮窗开发之窗口层级

    很多人都知道如何去实现一个简单的浮窗,但是却很少有人去深入的研究背后的流程机制,由于项目中浮窗交互比较复杂,遇到了些坑查看了很多资料,故总结浮窗涉及到的知识点: 窗口层级关系(浮窗是如何"浮 ...

  7. android 浮窗示例代码,Android 浮窗开发之窗口层级(示例代码)

    很多人都知道如何去实现一个简单的浮窗,但是却很少有人去深入的研究背后的流程机制,由于项目中浮窗交互比较复杂,遇到了些坑查看了很多资料,故总结浮窗涉及到的知识点: 窗口层级关系(浮窗是如何"浮 ...

  8. Android实现一键开启自由窗口、分屏、画中画模式——画中画模式

    转载请注明出处:https://blog.csdn.net/sunmmer123 Android实现一键开启自由窗口.分屏.画中画模式系列 一键开启进入自由窗口模式 一键开启进入分屏模式 一键进入画中 ...

  9. android实现 桌面移动悬浮窗口实现

    现在很多应用都有这样的功能,比如360等安全卫士,手机管家之内的应用. 效果图: 一.实现原理及移动思路 调用WindowManager,并设置WindowManager.LayoutParams的相 ...

最新文章

  1. : error c2062: 意外的类型“int”_Go 命令行解析 flag 包之扩展新类型
  2. 富士通台式电脑_英特尔X86架构霸权终将崩塌,ARM架构才是未来PC电脑市场的王者?...
  3. Vim的使用技巧-自动闭合成对符号
  4. Linux的md64进程,在Linux上安装Elasticsearch Kibaba.md(示例代码)
  5. 用于MCU,基于FreeRTOS的micro(轻量级)ROS
  6. 三、Express 路由
  7. CYQ.Data 轻量数据层之路 自定义MDataTable绑定续章(七)
  8. date对象 java_Java_按照指定的日期创建 Date对象
  9. python 怎么判断文件存在哪里_Python判断文件和文件夹是否存在的方法
  10. 【攻防世界】九、ext3
  11. 基于ARM的嵌入式Linux应用程序开发
  12. 电源管理允许此设备唤醒计算机怎么关掉,电脑如何设置电源管理允许鼠标唤醒计算机...
  13. c语言中循环指令m=_crol_(m,1),单片机中关于_crol_函数 aa=_crol_(aa,1),执行八次之后流水灯为什么回到了初始状态继续循环 ?...
  14. 软件工程导论复习之详细设计
  15. IBM服务器无法启动怎么恢复
  16. Android移动开发的几种方式
  17. 面试必备:虾皮服务端15连问
  18. 【从零开始学极狐gitlab】01环境搭建 #JIHULAB101
  19. 介绍篇 决策引擎环节
  20. 银行家算法##大魔王程序员

热门文章

  1. Java并发编程:volatile关键字解析
  2. 打破双亲委派机制有什么用_你确定你真的理解双亲委派了吗?!
  3. 吴恩达:AI要拥抱【高质量小数据】的训练范式
  4. 黑匣子解密要多久_解密“黑匣子”:黑匣子发展历史及如何定位打捞
  5. 狭义线性模型与广义线性模型
  6. ASCII GBK Unicode 等各种字符编码
  7. parallels desktop 虚拟机win11修改IP地址
  8. Stable Difussion 拒绝崩脸崩手崩身 ADetailer 插件
  9. 【C++】医学影像PACS管理系统源码支持三维图像后处理和重建
  10. bert tokenizer