Android特殊窗口之输入法窗口的添加策略

本篇基于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;
                }
                //创建输入法的WindowState
         final 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都为true
        final 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为false
            setInputMethodTarget(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也没有变化则直接return
        if (target == mInputMethodTarget && mInputMethodTargetWaitingAnim == targetWaitingAnim) {
            return;
        }

        mInputMethodTarget = target;
        mInputMethodTargetWaitingAnim = targetWaitingAnim;
        //setLayoutNeeded为false
        assignWindowLayers(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

@Override
    void assignChildLayers(SurfaceControl.Transaction t) {
		//设置三大窗口容器的Layer为0,1,2
        mBelowAppWindowsContainers.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来计算目标窗口,只是调用条件不同而已,我们就不再进行分析了。

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值