안드로이드 소프트 키보드 windowSoftInputMode 사용 및 원리(원리)

머리말

안드로이드 소프트 키보드 windowSoftInputMode의 사용 및 원리에 대한 이전 글에서는 windowSoftInputMode의 각 속성별 구체적인 기능을 소개하였고, 다음으로 각 속성의 구현 원리를 소스 코드 레벨(Android9 기준)에서 분석해 보겠습니다.

1. 가시성 원칙

소프트 키보드의 숨기기 및 표시를 분석하려면 먼저 Android 시스템의 소프트 키보드 아키텍처가 어떤 것인지 이해해야 합니다. 아래 그림을 참조하십시오. InputMethodManagerService(이하 IMMS라고도 함)는 입력 방법 서비스(IMS라고 하는 InputMethodService) 로드 및 전환을 포함하여 시스템의 모든 입력 방법 관리를 담당합니다
여기에 이미지 설명 삽입
. 프로그램이 포커스를 얻으면 InputMethodManager를 통해 포커스를 얻었음을 InputMethodManagerService에 알리고 현재 입력 방법에 바인딩하도록 요청합니다. 동시에 EditorView와 같이 입력 방식이 필요한 프로그램의 뷰가 포커스를 받으면 InputMethodManagerService에 InputMethodManager를 통해 입력 방식을 표시하도록 요청하고, 요청을 받은 후 InputMethodManagerService는 현재 입력 방식에 요청된 EditText 데이터 통신 인터페이스를 전송하고 입력 방식 표시를 요청합니다. 입력 방법은 요청을 받은 후 자체 UI 대화 상자를 표시하고 동시에 대상 뷰의 데이터 구조를 저장하며 사용자가 입력을 완료하면 문자가 뷰의 데이터 통신 인터페이스를 통해 해당 뷰로 직접 전달됩니다.

또한 Android 창 관리 시스템도 이해해야 합니다. 다음 그림을 참조하세요.
여기에 이미지 설명 삽입

WindowManagerService(이하 WMS라고도 함)는 매우 복잡하며 시스템의 모든 창을 관리하는 역할을 합니다.창이 변경될 때(포커스 상태 및 크기) 앱 프로세스에 알린다는 것만 알면 됩니다.

앱의 창, 즉 페이지의 Focus 상태가 변경되면(예: 새 창에 진입하면 이 창의 Focus 상태가 true가 되고 이전 창은 false가 됨) 이벤트는 ViewRootImpl의 WindowInputEventReceiver로 다시 호출됩니다. 일련의 호출 후 IMMS의 startInputOrWindowGainedFocusInternalLocked 메서드로 들어갑니다. 이 메서드의 핵심 코드를 살펴보세요.

    @NonNull
    private InputBindResult startInputOrWindowGainedFocusInternalLocked(
            @StartInputReason int startInputReason, IInputMethodClient client,
            @NonNull IBinder windowToken, @StartInputFlags int startInputFlags,
            @SoftInputModeFlags int softInputMode, int windowFlags, EditorInfo attribute,
            IInputContext inputContext, @MissingMethodFlags int missingMethods,
            int unverifiedTargetSdkVersion, @UserIdInt int userId) {
    
    
            
        ......省略........
        
        InputBindResult res = null;
        switch (softInputMode & LayoutParams.SOFT_INPUT_MASK_STATE) {
    
    
            case LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
                if (!isTextEditor || !doAutoShow) {
    
    
                    if (LayoutParams.mayUseInputMethod(windowFlags)) {
    
    
                        // There is no focus view, and this window will
                        // be behind any soft input window, so hide the
                        // soft input window if it is shown.
                        if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
                        hideCurrentInputLocked(
                                mCurFocusedWindow, InputMethodManager.HIDE_NOT_ALWAYS, null,
                                SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);

                        // If focused display changed, we should unbind current method
                        // to make app window in previous display relayout after Ime
                        // window token removed.
                        // Note that we can trust client's display ID as long as it matches
                        // to the display ID obtained from the window.
                        if (cs.selfReportedDisplayId != mCurTokenDisplayId) {
    
    
                            unbindCurrentMethodLocked();
                        }
                    }
                } else if (isTextEditor && doAutoShow
                        && (softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
    
    
                    // There is a focus view, and we are navigating forward
                    // into the window, so show the input window for the user.
                    // We only do this automatically if the window can resize
                    // to accommodate the IME (so what the user sees will give
                    // them good context without input information being obscured
                    // by the IME) or if running on a large screen where there
                    // is more room for the target window + IME.
                    if (DEBUG) Slog.v(TAG, "Unspecified window will show input");
                    if (attribute != null) {
    
    
                        res = startInputUncheckedLocked(cs, inputContext, missingMethods,
                                attribute, startInputFlags, startInputReason);
                        didStart = true;
                    }
                    showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
                            SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
                }
                break;
            case LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
                // Do nothing.
                break;
            case LayoutParams.SOFT_INPUT_STATE_HIDDEN:
                if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
    
    
                    if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
                    hideCurrentInputLocked(mCurFocusedWindow, 0, null,
                            SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV);
                }
                break;
            case LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
                if (DEBUG) Slog.v(TAG, "Window asks to hide input");
                hideCurrentInputLocked(mCurFocusedWindow, 0, null,
                        SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE);
                break;
            case LayoutParams.SOFT_INPUT_STATE_VISIBLE:
                if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
    
    
                    if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");
                    if (InputMethodUtils.isSoftInputModeStateVisibleAllowed(
                            unverifiedTargetSdkVersion, startInputFlags)) {
    
    
                        if (attribute != null) {
    
    
                            res = startInputUncheckedLocked(cs, inputContext, missingMethods,
                                    attribute, startInputFlags, startInputReason);
                            didStart = true;
                        }
                        showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
                                SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV);
                    } else {
    
    
                        Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
                                + " there is no focused view that also returns true from"
                                + " View#onCheckIsTextEditor()");
                    }
                }
                break;
            case LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
                if (DEBUG) Slog.v(TAG, "Window asks to always show input");
                if (InputMethodUtils.isSoftInputModeStateVisibleAllowed(
                        unverifiedTargetSdkVersion, startInputFlags)) {
    
    
                    if (attribute != null) {
    
    
                        res = startInputUncheckedLocked(cs, inputContext, missingMethods,
                                attribute, startInputFlags, startInputReason);
                        didStart = true;
                    }
                    showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
                            SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE);
                } else {
    
    
                    Slog.e(TAG, "SOFT_INPUT_STATE_ALWAYS_VISIBLE is ignored because"
                            + " there is no focused view that also returns true from"
                            + " View#onCheckIsTextEditor()");
                }
                break;
        }

여기에서 windowSoftInputMode의 6개의 stateXXX가 처리되는 것을 볼 수 있는데, 윈도우의 포커스 상태가 true(앞으로든 뒤로든)가 되면 여기로 갑니다. 하나씩 분석해 보겠습니다.

1. 상태 변경되지 않음

표시 여부에 관계없이 활동이 전경에 올 때 소프트 키보드의 마지막 상태를 유지합니다.

case LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
                // Do nothing.
                break;

여기에서 예약된 상태는 아무것도 하지 않고 아무 작업도 수행하지 않는 것을 볼 수 있습니다.

2.stateHidden과 stateAlwaysHidden

사용자가 활동을 선택할 때, 즉 사용자가 다른 활동에서 나갔다가 돌아오는 것이 아니라 실제로 해당 활동으로 이동할 때 소프트 키보드를 숨깁니다.

 case LayoutParams.SOFT_INPUT_STATE_HIDDEN:
                if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
    
    
                    if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
                    hideCurrentInputLocked(mCurFocusedWindow, 0, null,
                            SoftInputShowHideReason.HIDE_STATE_HIDDEN_FORWARD_NAV);
                }
                break;
 case LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
                if (DEBUG) Slog.v(TAG, "Window asks to hide input");
                hideCurrentInputLocked(mCurFocusedWindow, 0, null,
                        SoftInputShowHideReason.HIDE_ALWAYS_HIDDEN_STATE);
                break;

코드 관점에서 stateHidden은 stateAlwaysHidden보다 LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION의 한 번 더 판단한 것입니다. method는 input method를 숨기지만 stateAlwaysHidden은 이런 판단이 없고, 어쨌든 윈도우가 포커스를 받아 input method를 숨길 것이다.

3.stateVisible과 stateAlwaysVisible

사용자가 활동을 선택할 때, 즉 사용자가 다른 활동에서 나갔다가 돌아오는 것이 아니라 실제로 해당 활동으로 앞으로 이동하면 소프트 키보드가 표시됩니다.
활동의 기본 창에 입력 포커스가 있을 때 항상 소프트 키보드를 표시합니다.

case LayoutParams.SOFT_INPUT_STATE_VISIBLE:
                if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
    
    
                    if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");
                    if (InputMethodUtils.isSoftInputModeStateVisibleAllowed(
                            unverifiedTargetSdkVersion, startInputFlags)) {
    
    
                        if (attribute != null) {
    
    
                            res = startInputUncheckedLocked(cs, inputContext, missingMethods,
                                    attribute, startInputFlags, startInputReason);
                            didStart = true;
                        }
                        showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
                                SoftInputShowHideReason.SHOW_STATE_VISIBLE_FORWARD_NAV);
                    } else {
    
    
                        Slog.e(TAG, "SOFT_INPUT_STATE_VISIBLE is ignored because"
                                + " there is no focused view that also returns true from"
                                + " View#onCheckIsTextEditor()");
                    }
                }
                break;
 case LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
                if (DEBUG) Slog.v(TAG, "Window asks to always show input");
                if (InputMethodUtils.isSoftInputModeStateVisibleAllowed(
                        unverifiedTargetSdkVersion, startInputFlags)) {
    
    
                    if (attribute != null) {
    
    
                        res = startInputUncheckedLocked(cs, inputContext, missingMethods,
                                attribute, startInputFlags, startInputReason);
                        didStart = true;
                    }
                    showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
                            SoftInputShowHideReason.SHOW_STATE_ALWAYS_VISIBLE);
                } else {
    
    
                    Slog.e(TAG, "SOFT_INPUT_STATE_ALWAYS_VISIBLE is ignored because"
                            + " there is no focused view that also returns true from"
                            + " View#onCheckIsTextEditor()");
                }
                break;

숨김과 마찬가지로 표시도 같은 루틴이므로 여기서는 더 이상 언급하지 않겠습니다.
표시되면 isSoftInputModeStateVisibleAllowed 메서드를 호출하여 표시가 허용되는지 판단합니다. 이 메서드를 살펴보세요.

    static boolean isSoftInputModeStateVisibleAllowed(int targetSdkVersion,
            @StartInputFlags int startInputFlags) {
    
    
        if (targetSdkVersion < Build.VERSION_CODES.P) {
    
    
            // for compatibility.
            return true;
        }
        if ((startInputFlags & StartInputFlags.VIEW_HAS_FOCUS) == 0) {
    
    
            return false;
        }
        if ((startInputFlags & StartInputFlags.IS_TEXT_EDITOR) == 0) {
    
    
            return false;
        }
        return true;
    }

안드로이드 9 이하에서는 바로 true를 반환하고, 안드로이드 9 이상에서는 2가지 판단이 추가되는데, 안드로이드 9 이후 소프트 키보드가 자동으로 뜨지 않는 이유에 대한 설명입니다.
판정을 입력한 후 입력기 처리가 활성화되어 있는지 판단하고 활성화되어 있지 않으면 startInputUncheckedLocked를 호출하여 활성화하고 활성화 후 showCurrentInputLocked로 표시된 소프트 키보드를 제거합니다.

4.상태 미지정

소프트 키보드의 상태(숨김 또는 표시)가 지정되지 않았습니다. 적절한 상태를 선택하거나 테마의 설정에 의존하는 것은 시스템에 달려 있습니다.
시스템은 스스로 표시하거나 숨기는 것을 선택하므로 여기서는 소개하지 않겠으며 관심 있는 분들은 스스로 공부하시면 됩니다.

case LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
                if (!isTextEditor || !doAutoShow) {
    
    
                    if (LayoutParams.mayUseInputMethod(windowFlags)) {
    
    
                        // There is no focus view, and this window will
                        // be behind any soft input window, so hide the
                        // soft input window if it is shown.
                        if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
                        hideCurrentInputLocked(
                                mCurFocusedWindow, InputMethodManager.HIDE_NOT_ALWAYS, null,
                                SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);

                        // If focused display changed, we should unbind current method
                        // to make app window in previous display relayout after Ime
                        // window token removed.
                        // Note that we can trust client's display ID as long as it matches
                        // to the display ID obtained from the window.
                        if (cs.selfReportedDisplayId != mCurTokenDisplayId) {
    
    
                            unbindCurrentMethodLocked();
                        }
                    }
                } else if (isTextEditor && doAutoShow
                        && (softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
    
    
                    // There is a focus view, and we are navigating forward
                    // into the window, so show the input window for the user.
                    // We only do this automatically if the window can resize
                    // to accommodate the IME (so what the user sees will give
                    // them good context without input information being obscured
                    // by the IME) or if running on a large screen where there
                    // is more room for the target window + IME.
                    if (DEBUG) Slog.v(TAG, "Unspecified window will show input");
                    if (attribute != null) {
    
    
                        res = startInputUncheckedLocked(cs, inputContext, missingMethods,
                                attribute, startInputFlags, startInputReason);
                        didStart = true;
                    }
                    showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
                            SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
                }
                break;

2. 디스플레이 모드의 원리

표시 방법은 더 복잡하고 많은 측면을 포함합니다.우선 PhoneWindowManager의 layoutWindowLw 방법(시스템 프로세스에 속함)에서 이 방법은 주로 창의 일부 위치 및 크기 매개 변수를 설정합니다.소프트 키보드가 팝업되거나 닫힐 때 여기로 이동하여 창 크기를 재설정합니다.간단한 관련 코드를 살펴보십시오.

public void layoutWindowLw(WindowState win, WindowState attached, DisplayFrames displayFrames) {
    
    

        final int type = attrs.type;
        final int fl = PolicyControl.getWindowFlags(win, attrs);
        final int pfl = attrs.privateFlags;
        final int sim = attrs.softInputMode;
        final int requestedSysUiFl = PolicyControl.getSystemUiVisibility(null, attrs);
        final int sysUiFl = requestedSysUiFl | getImpliedSysUiFlagsForLayout(attrs);

        final Rect pf = mTmpParentFrame;
        final Rect df = mTmpDisplayFrame;
        final Rect of = mTmpOverscanFrame;
        final Rect cf = mTmpContentFrame;
        final Rect vf = mTmpVisibleFrame;
        final Rect dcf = mTmpDecorFrame;
        final Rect sf = mTmpStableFrame;
        Rect osf = null;
        dcf.setEmpty();

        final boolean hasNavBar = (isDefaultDisplay && mHasNavigationBar
                && mNavigationBar != null && mNavigationBar.isVisibleLw());

        final int adjust = sim & SOFT_INPUT_MASK_ADJUST;

        final boolean requestedFullscreen = (fl & FLAG_FULLSCREEN) != 0
                || (requestedSysUiFl & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0;

        final boolean layoutInScreen = (fl & FLAG_LAYOUT_IN_SCREEN) == FLAG_LAYOUT_IN_SCREEN;
        final boolean layoutInsetDecor = (fl & FLAG_LAYOUT_INSET_DECOR) == FLAG_LAYOUT_INSET_DECOR;

        sf.set(displayFrames.mStable);

        if (adjust != SOFT_INPUT_ADJUST_RESIZE) {
    
    
            cf.set(displayFrames.mDock);
        } else {
    
    
            cf.set(displayFrames.mContent);
        }
        applyStableConstraints(sysUiFl, fl, cf, displayFrames);
        if (adjust != SOFT_INPUT_ADJUST_NOTHING) {
    
    
            vf.set(displayFrames.mCurrent);
        } else {
    
    
            vf.set(cf);
        }
    }

우선 mTmpXXXFrame에 의해 할당된 메서드에는 Rect 타입의 파라미터가 많다 Rect는 직사각형 영역을 나타내는 데 사용되며, 이는 윈도우의 크기를 나타내는 데 사용되는 것으로 알고 있다. 에 따라 입력 방식이 팝업되거나 접힐 때 여기의 크기는 설정된 windowSoftInputMode에 따라 변경되며 최종적으로 창 크기 변경은 응용 프로세스의 ViewRootImpl로 다시 호출되고 마지막으로 ViewRootHandler의 MSG_RESIZED 이벤트로 다시 호출 됩니다 .

1. 크기 조정

위 코드에 따르면 SOFT_INPUT_ADJUST_RESIZE를 설정하면 cf(콘텐츠 영역)은 displayFrames.mContent(표시 영역)로 , vf(가시 영역)는 displayFrames.mCurrent로 설정되는 것을 확인 했습니다. , 그러나 콘텐츠 영역이 더 작아지지는 않습니다. 소프트 키보드로 덮여 있지만 콘텐츠 영역은 계속 사용할 수 있습니다. 여기서 cf는 displayFrames.mContent로 설정되어 소프트 키보드로 가려진 곳은 사용하면 안된다는 의미입니다.

    public void handleMessage(Message msg) {
    
    
        switch (msg.what) {
    
    
            ........
            case MSG_RESIZED: {
    
    
                // Recycled in the fall through...
                SomeArgs args = (SomeArgs) msg.obj;
                if (mWinFrame.equals(args.arg1)
                        && mPendingOverscanInsets.equals(args.arg5)
                        && mPendingContentInsets.equals(args.arg2)
                        && mPendingStableInsets.equals(args.arg6)
                        && mPendingDisplayCutout.get().equals(args.arg9)
                        && mPendingVisibleInsets.equals(args.arg3)
                        && mPendingOutsets.equals(args.arg7)
                        && mPendingBackDropFrame.equals(args.arg8)
                        && args.arg4 == null
                        && args.argi1 == 0
                        && mDisplay.getDisplayId() == args.argi3) {
    
    
                    break;
                }
            } // fall through...
            case MSG_RESIZED_REPORT:
                if (mAdded) {
    
    
                    SomeArgs args = (SomeArgs) msg.obj;

                    final int displayId = args.argi3;
                    MergedConfiguration mergedConfiguration = (MergedConfiguration) args.arg4;
                    final boolean displayChanged = mDisplay.getDisplayId() != displayId;

                    if (!mLastReportedMergedConfiguration.equals(mergedConfiguration)) {
    
    
                        // If configuration changed - notify about that and, maybe,
                        // about move to display.
                        performConfigurationChange(mergedConfiguration, false /* force */,
                                displayChanged
                                        ? displayId : INVALID_DISPLAY /* same display */);
                    } else if (displayChanged) {
    
    
                        // Moved to display without config change - report last applied one.
                        onMovedToDisplay(displayId, mLastConfigurationFromResources);
                    }

                    final boolean framesChanged = !mWinFrame.equals(args.arg1)
                            || !mPendingOverscanInsets.equals(args.arg5)
                            || !mPendingContentInsets.equals(args.arg2)
                            || !mPendingStableInsets.equals(args.arg6)
                            || !mPendingDisplayCutout.get().equals(args.arg9)
                            || !mPendingVisibleInsets.equals(args.arg3)
                            || !mPendingOutsets.equals(args.arg7);

                    mWinFrame.set((Rect) args.arg1);
                    mPendingOverscanInsets.set((Rect) args.arg5);
                    mPendingContentInsets.set((Rect) args.arg2);
                    mPendingStableInsets.set((Rect) args.arg6);
                    mPendingDisplayCutout.set((DisplayCutout) args.arg9);
                    mPendingVisibleInsets.set((Rect) args.arg3);
                    mPendingOutsets.set((Rect) args.arg7);
                    mPendingBackDropFrame.set((Rect) args.arg8);
                    mForceNextWindowRelayout = args.argi1 != 0;
                    mPendingAlwaysConsumeNavBar = args.argi2 != 0;

                    args.recycle();

                    if (msg.what == MSG_RESIZED_REPORT) {
    
    
                        reportNextDraw();
                    }

                    if (mView != null && framesChanged) {
    
    
                        forceLayout(mView);
                    }
                    requestLayout();
                }
                break;
            .........
        }
    }

SomeArgs의 args.arg2는 이전에 설정한 cf입니다.
1. MSG_RESIZED 입력 후 SomeArgs의 매개 변수가 이전과 동일한지 여부를 판단합니다. 다르면 MSG_RESIZED_REPORT로 이동합니다. 2. 여기에 크기 정보를 저장한 후 forceLayout(mView)—>ViewTree의 각 View/ViewGroup에 대한 레이아웃 및 그리기를 호출합니다. 즉, 각 View/ViewGroup은 마지막에 세 가지 주요 프로세스를 실행합니다. , requestLayout()—> 세 가지 주요 프로세스의 실행을 트리거합니다
.

이제 크기 변경 사항이 문서화되었으므로 이러한 값이 어떻게 사용되는지 추적하십시오. requestLayout()을 호출하면 performTraversals() 메서드 실행이 트리거됩니다.

#ViewRootImpl.java
    private void performTraversals() {
    
    
        if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
    
    
            ...
            boolean hwInitialized = false;
            //内容边界是否发生变化
            boolean contentInsetsChanged = false;
            try {
    
    
                ...
                //内容区域变化----------->1
                contentInsetsChanged = !mPendingContentInsets.equals(
                        mAttachInfo.mContentInsets);

                if (contentInsetsChanged || mLastSystemUiVisibility !=
                        mAttachInfo.mSystemUiVisibility || mApplyInsetsRequested
                        || mLastOverscanRequested != mAttachInfo.mOverscanRequested
                        || outsetsChanged) {
    
    
                    ...
                    //分发Inset----------->2
                    dispatchApplyInsets(host);
                    contentInsetsChanged = true;
                }
                ...
            } catch (RemoteException e) {
    
    
            }
            ...
        }
        ...
    }

1. SOFT_INPUT_ADJUST_RESIZE가 설정되면 키보드 팝업 시 컨텐츠 영역이 변경되므로 dispatchApplyInsets()가 실행됩니다.
2. 인셋을 배포합니다.
이 레코드의 값은 AttachInfo에 해당하는 변수에 저장됩니다.
이 메서드의 호출 스택은 다음과 같습니다.
여기에 이미지 설명 삽입
dispatchApplyWindowInsets(WindowInsets insets)의 인셋은 계산 전에 mPendingXX에 기록된 경계 값으로 구성됩니다.
마지막으로 fitSystemWindowsInt()를 호출합니다.

#View.java
    private boolean fitSystemWindowsInt(Rect insets) {
    
    
        //FITS_SYSTEM_WINDOWS 为xml里设置     android:fitsSystemWindows="true"
        //对于DecorView的子布局LinearLayout来说,默认fitsSystemWindows=true
        if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
    
    
            ...
            //设置View的padding
            internalSetPadding(localInsets.left, localInsets.top,
                    localInsets.right, localInsets.bottom);
            return res;
        }
        return false;
    }

    protected void internalSetPadding(int left, int top, int right, int bottom) {
    
    
        ...
        if (mPaddingLeft != left) {
    
    
            changed = true;
            mPaddingLeft = left;
        }
        if (mPaddingTop != top) {
    
    
            changed = true;
            mPaddingTop = top;
        }
        if (mPaddingRight != right) {
    
    
            changed = true;
            mPaddingRight = right;
        }
        if (mPaddingBottom != bottom) {
    
    
            changed = true;
            mPaddingBottom = bottom;
        }

        if (changed) {
    
    
            requestLayout();
            invalidateOutline();
        }
    }

이 답변을 보면 답이 나올 준비가 된 것입니다.DecoView의 Sub-layout LinearLayout은 padding을 설정하는데, 이는 결국 LinearLayout sub-layout의 높이에 영향을 미치게 됩니다.이것이 레이어별로 전달되면 Demo에서 Activity 레이아웃 파일의 높이에 영향을 미치게 됩니다.

요약
SOFT_INPUT_ADJUST_RESIZE를 설정하면 DecorView의 하위 레이아웃 패딩이 변경되고 최종적으로 하위 레이아웃의 높이에 영향을 미칩니다.

2.adjustPan

SOFT_INPUT_ADJUST_PAN이 설정되면 cf(콘텐츠 영역)는 displayFrames.mDock으로 설정되고 vf (가시 영역)도 displayFrames.mCurrent로 설정됩니다 .

    /** During layout, the current screen borders along which input method windows are placed. */
    public final Rect mDock = new Rect();

레이아웃 중에는 입력기 창이 위치한 현재 화면 테두리 앱이 내비게이션 바와 상태 표시줄을 제외한 콘텐츠 영역을 사용할 수 있는 것으로 알고 있습니다 나중에 중단점 확인을 통해 사실입니다 SOFT_INPUT_ADJUST_RESIZE는 cf를 보이는 영역으로 설정하고 여기서 SOFT_INPUT_ADJUST_PAN cf는 콘텐츠 영역으로 설정하여 앱이 입력기에 의해 차단된 부분을 사용할 수 있음을 의미합니다.
결국 MSG_RESIZED:
args.arg2 in SomeArgs, 즉 cf의 크기는 변경되지 않았지만 args.arg3, 즉 vf는 변경되었으므로 여전히 MSG_RESIZED_REPORT에 들어가고 여전히 forceLayout(mView) 및 requestLayout()을 호출하여 세 가지 주요 프로세스의 실행을 트리거합니다.

그러나 콘텐츠 영역이 변경되지 않았으므로 여기서는 dispatchApplyInsets()가 실행되지 않습니다. SOFT_INPUT_ADJUST_PAN의 처리는 어디에 있습니까?
우리는 SOFT_INPUT_ADJUST_PAN의 효과가 페이지 전체를 위쪽으로 이동시키는 것임을 알고 있습니다.레이아웃 이동은 좌표의 변경이나 내용의 스크롤에 지나지 않습니다.형식에 관계없이 이동 효과를 얻으려면 캔버스를 옮겨야 합니다. performDraw->draw 메서드에는 scrollToRectOrFocus 메서드가 있습니다.

#View.java
    boolean scrollToRectOrFocus(Rect rectangle, boolean immediate) {
    
    
        //窗口内容区域
        final Rect ci = mAttachInfo.mContentInsets;
        //窗口可见区域
        final Rect vi = mAttachInfo.mVisibleInsets;
        //滚动距离
        int scrollY = 0;
        boolean handled = false;

        if (vi.left > ci.left || vi.top > ci.top
                || vi.right > ci.right || vi.bottom > ci.bottom) {
    
    
            scrollY = mScrollY;
            //找到当前有焦点的View------------>(1)
            final View focus = mView.findFocus();
            ...
            if (focus == lastScrolledFocus && !mScrollMayChange && rectangle == null) {
    
    
                //焦点没有发生切换,不做操作
            } else {
    
    
                // We need to determine if the currently focused view is
                // within the visible part of the window and, if not, apply
                // a pan so it can be seen.
                mLastScrolledFocus = new WeakReference<View>(focus);
                mScrollMayChange = false;
                if (DEBUG_INPUT_RESIZE) Log.v(mTag, "Need to scroll?");
                // Try to find the rectangle from the focus view.
                if (focus.getGlobalVisibleRect(mVisRect, null)) {
    
    
                    ...
                    //找到当前焦点与可见区域的相交部分
                    //mVisRect 为当前焦点在Window里的可见部分
                    if (mTempRect.intersect(mVisRect)) {
    
    
                        if (mTempRect.height() >
                                (mView.getHeight()-vi.top-vi.bottom)) {
    
    
                            ...
                        }
                        else if (mTempRect.top < vi.top) {
    
    
                            //如果当前焦点位置在窗口可见区域上边,说明焦点View应该往下移动到可见区域里边
                            scrollY = mTempRect.top - vi.top;
                        } else if (mTempRect.bottom > (mView.getHeight()-vi.bottom)) {
    
    
                            //如果当前焦点位置在窗口可见区域之下,说明其应该往上移动到可见区域里边------->(2)
                            scrollY = mTempRect.bottom - (mView.getHeight()-vi.bottom);
                        } else {
    
    
                            //无需滚动------->(3)
                            scrollY = 0;
                        }
                        handled = true;
                    }
                }
            }
        }

        if (scrollY != mScrollY) {
    
    
            //滚动距离发生变化
            if (!immediate) {
    
    
                if (mScroller == null) {
    
    
                    mScroller = new Scroller(mView.getContext());
                }
                //开始设置滚动----------->(4)
                mScroller.startScroll(0, mScrollY, 0, scrollY-mScrollY);
            } else if (mScroller != null) {
    
    
                mScroller.abortAnimation();
            }
            //赋值给成员变量
            mScrollY = scrollY;
        }
        return handled;
    }

1. 위 데모의 경우 현재 초점 보기는 EditText이며 어떤 EditText를 클릭하면 초점이 맞춰집니다.
2. 인풋 박스 2의 경우 키보드가 튀어나와 덮을 것이기 때문에 계산은 "현재 포커스 위치가 창의 보이는 영역 아래에 있어 보이는 영역까지 위로 이동해야 함을 나타냅니다"라는 조건을 만족하므로 srolly > 0입니다.
3. 인풋 박스 1은 키보드가 팝업되면 키보드로 막히지 않고 else 분기로 가므로 scrollY = 0이다.
4. 스크롤링은 Scoller.java 클래스의 도움으로 이루어집니다.

위의 작업은 실제로 스크롤 값을 확인하고 멤버 변수 mScrollY에 기록하는 것인데 계속해서 스크롤 값을 어떻게 사용하는지 알아볼까요?

#ViewRootImpl.java
    private boolean draw(boolean fullRedrawNeeded) {
    
    
        ...
        boolean animating = mScroller != null && mScroller.computeScrollOffset();
        final int curScrollY;
        //获取当前需要滚动的scroll值
        if (animating) {
    
    
            curScrollY = mScroller.getCurrY();
        } else {
    
    
            curScrollY = mScrollY;
        }
        ...
        
        int xOffset = -mCanvasOffsetX;
        //记录在yOffset里
        int yOffset = -mCanvasOffsetY + curScrollY;
        
        boolean useAsyncReport = false;
        if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
    
    
            if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) {
    
    
                ...
                //对于走硬件加速绘制
                if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) {
    
    
                    //记录偏移量到mHardwareYOffset里
                    mHardwareYOffset = yOffset;
                    mHardwareXOffset = xOffset;
                    invalidateRoot = true;
                }
                ..
                mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
            } else {
    
    
                //软件绘制
                //传入yOffset
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
                        scalingRequired, dirty, surfaceInsets)) {
    
    
                    return false;
                }
            }
        }
        ...
        return useAsyncReport;
    }

스크롤 값은 각각 하드웨어 가속 드로잉 분기와 소프트웨어 드로잉 분기로 전달되고 캔버스는 해당 분기에서 변환됩니다. 변환 방법은 여기에서 자세히 설명하지 않으며 코드 분석을 계속 수행할 수 있습니다.

요약

1. SOFT_INPUT_ADJUST_PAN이 설정되었을 때 키보드가 현재 포커스된 View를 덮는 것으로 확인되면 RootView의 Canvas(여기서는 Demo의 DecorView가 RootView로 사용됨)가 포커스된 View가 가시 영역에 표시될 때까지 변환됩니다.
2. 입력 상자 2를 클릭하면 레이아웃이 전체적으로 위로 이동하는 이유입니다.

3.adjustNothing

SOFT_INPUT_ADJUST_NOTHING으로 설정하면 cf(콘텐츠 영역)는 displayFrames.mDock으로 설정되고 vf (가시 영역)는 cf로 설정됩니다. 사실 이는 창 크기 매개변수가 변경되지 않았음을 의미하므로 MSG_RESIZED로 다시 호출되지 않으며 자연스러운 레이아웃이 변경되지 않습니다.

4.adjust미지정

SOFT_INPUT_ADJUST_UNSPECIFIED가 설정되어 내부적으로 SOFT_INPUT_ADJUST_PAN 및 SOFT_INPUT_ADJUST_RESIZE 중 하나를 사용하여 표시됩니다. 다음으로 선택 기준이 무엇인지 살펴보겠습니다.
softInputMode가 설정되지 않은 경우 기본값은 SOFT_INPUT_ADJUST_UNSPECIFIED 모드입니다.
여전히 ViewRootImpl.java의 performTraversals() 메서드에서 분석을 시작합니다.

    private void performTraversals() {
    
    
        ...
        if (mFirst || mAttachInfo.mViewVisibilityChanged) {
    
    
            mAttachInfo.mViewVisibilityChanged = false;
            //先查看有没有提前设置了模式
            int resizeMode = mSoftInputMode &
                    WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
            
            //如果没有设置,那么默认为0,也就是SOFT_INPUT_ADJUST_UNSPECIFIED
            if (resizeMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED) {
    
    
                //查看mScrollContainers 数组有没有元素(View 作为元素) -------->(1)
                final int N = mAttachInfo.mScrollContainers.size();
                for (int i=0; i<N; i++) {
    
    
                    if (mAttachInfo.mScrollContainers.get(i).isShown()) {
    
    
                        //如果有元素,则设置为SOFT_INPUT_ADJUST_RESIZE 模式
                        resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
                    }
                }
                if (resizeMode == 0) {
    
    
                    //如果没有设置为resize模式,则设置pan模式
                    resizeMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
                }
                if ((lp.softInputMode &
                        WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) != resizeMode) {
    
    
                    lp.softInputMode = (lp.softInputMode &
                            ~WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST) |
                            resizeMode;
                    //最后赋值给params,让Window属性生效
                    params = lp;
                }
            }
        }
        ...
    }

여기서는 앞선 글에서 언급했듯이 주로 mAttachInfo.mScrollContainers를 통해 scrollable view가 있는지 판단하는 것으로, 있다면 SOFT_INPUT_ADJUST_RESIZE로 설정하고 없으면 SOFT_INPUT_ADJUST_PAN으로 설정합니다.

추천

출처blog.csdn.net/shanshui911587154/article/details/127537625