Android透明狀態(tài)欄和軟鍵盤配合的坑

一般我們?yōu)榱俗屾I盤自動(dòng)將界面彈起,會(huì)在清單文件中配置windowSoftInputMode,配置為adjustResize或adjustPan。

  1. adjustResize,鍵盤彈起時(shí),將界面Layout高度壓縮,留出空間顯示軟鍵盤。
  2. adjustPan,需要存在滾動(dòng)控件,鍵盤彈起時(shí),滾動(dòng)列表,留出控件顯示軟鍵盤,如果沒有滾動(dòng)控件,則將全部控件上移,而不會(huì)壓縮界面Layout。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hule.dashi.live">

    <application>
        <activity
            android:name=".AudioLiveRoomActivity"
            android:windowSoftInputMode="adjustResize" />
    </application>
</manifest>

遇到的問題

在直播模塊開發(fā)中,需要將狀態(tài)欄透明。然而透明后,卻發(fā)現(xiàn)一個(gè)大坑,軟鍵盤模式失效了。無論windowSoftInputMode設(shè)置adjustResize還是adjustPan,鍵盤都會(huì)將界面覆蓋,而不會(huì)將輸入框頂起。

  • 搜索了一番,結(jié)果發(fā)現(xiàn)是谷歌在2.2年代就留下的坑,卻一直沒修復(fù)...那么我們是不是可以監(jiān)聽軟鍵盤彈起事件,獲取軟鍵盤高度,手動(dòng)壓縮布局Layout高度不就可以了?

  • 結(jié)果Android并沒有提供軟鍵盤彈起的監(jiān)聽事件,擦,這么坑的?那么還有什么辦法么?

辦法還是有的

搜索了一下,原來已經(jīng)有大神提供了處理類,名叫AndroidBug5497Workaround,大概原理就是給Activity的根View注冊(cè)ViewTreeObserver.OnGlobalLayoutListener。鍵盤彈起時(shí),監(jiān)聽會(huì)回調(diào),我們拿取原Activity始布局高度和Activity可見高度進(jìn)行相減,即可計(jì)算出鍵盤高度,再將布局高度重新設(shè)置,就可以將布局上移。同時(shí)可以將鍵盤改變作為回調(diào)監(jiān)聽提供給外部。

  • 結(jié)果又發(fā)現(xiàn)問題,在劉海屏上有問題,底部會(huì)空出一大片,而我修改后的做法是:計(jì)算出鍵盤高度后,給布局底部控件設(shè)置一個(gè)MarginBottom,自然會(huì)有一種頂出輸入框的效果。

源碼解析

首先是生成軟鍵盤的監(jiān)聽。提供給外部監(jiān)聽軟鍵盤監(jiān)聽。

  1. 首先我們傳入rootView構(gòu)造KeyboardFix。
  2. 給rootView設(shè)置addOnGlobalLayoutListener回調(diào)。
  3. addOnGlobalLayoutListener的onGlobalLayout回調(diào)時(shí),就是鍵盤彈起和下降。
  4. possiblyResizeChildOfContent(),computeUsableHeight()計(jì)算可見高度。
  5. 可見高度小于原始高度一定閥值,則當(dāng)做為鍵盤彈起。否則為下降軟鍵盤。
  6. 最后回調(diào)監(jiān)聽者。
public class KeyboardFix implements ViewTreeObserver.OnGlobalLayoutListener {
    /**
     * 父容器
     */
    private View vRootView;
    /**
     * 父容器的高度
     */
    private int mContentHeight;
    /**
     * 標(biāo)記,第一次回調(diào)時(shí)保存父容器的高度
     */
    private boolean isFirst = true;
    /**
     * 最后一次顯示父容器的高度,用于判斷是否顯示虛擬鍵盤
     */
    private int mLastShowHeight;

    //1.首先我們傳入rootView構(gòu)造KeyboardFix。
    public KeyboardFix(View rootView) {
        if (rootView != null) {
            //2. 給rootView設(shè)置addOnGlobalLayoutListener回調(diào)。
            rootView.getViewTreeObserver().addOnGlobalLayoutListener(this);
        }
    }
    
    @Override
    public void onGlobalLayout() {
        //3. addOnGlobalLayoutListener的onGlobalLayout回調(diào)時(shí),就是鍵盤彈起和下降。
        possiblyResizeChildOfContent();
    }
    
    /**
     * 重新調(diào)整跟布局的高度
     */
    private void possiblyResizeChildOfContent() {
        //4. possiblyResizeChildOfContent(),computeUsableHeight()計(jì)算可見高度。
        //計(jì)算內(nèi)容的可見高度
        int usableHeightNow = computeUsableHeight();
        if (isFirst) {
            //兼容華為等機(jī)型
            mContentHeight = usableHeightNow;
            isFirst = false;
        }
        //沒有改變,忽略
        if (usableHeightNow == mLastShowHeight) {
            return;
        }
        mLastShowHeight = usableHeightNow;
        //5. 可見高度小于原始高度一定閥值,則當(dāng)做為鍵盤彈起。否則為下降軟鍵盤。
        boolean isShowKeyboard = usableHeightNow + 200 < mContentHeight;
        //6. 最后回調(diào)監(jiān)聽者。
        for (OnKeyboardChangeCallback listener : mOnKeyboardChangeCallbacks) {
            listener.onChange(isShowKeyboard, mContentHeight, usableHeightNow);
        }
    }
    
     /**
     * 計(jì)算內(nèi)容的可見高度
     *
     * @return 父容器的可見高度
     */
    private int computeUsableHeight() {
        Rect rect = new Rect();
        vRootView.getWindowVisibleDisplayFrame(rect);
        return (rect.bottom - rect.top);
    }
    
    /**
     * 虛擬鍵盤顯示隱藏的監(jiān)聽
     */
    public interface OnKeyboardChangeCallback {
        /**
         * 虛擬鍵盤顯示發(fā)生變化時(shí)調(diào)用
         *
         * @param isVisible     true為可見,false為不可見
         * @param contentHeight 原始內(nèi)容高度
         * @param usableHeight  當(dāng)前可用高度
         */
        void onChange(boolean isVisible, int contentHeight, int usableHeight);

        /**
         * 界面暫時(shí)回調(diào)
         */
        void onPause();

        /**
         * 界面銷毀時(shí)回調(diào)
         */
        void onDestroy();
    }
}

拓展KeyboardFix,處理輸入框彈起

平時(shí)我們使用軟鍵盤模式,最多就是將輸入框上移,而透明狀態(tài)欄讓鍵盤模式失效,那么讓輸入框上移的活,就只能我們自己做了。

原理:

  1. 例如我們給輸入框設(shè)置一個(gè)父容器,點(diǎn)擊輸入框彈起軟鍵盤,這時(shí)我們的鍵盤回調(diào)會(huì)回調(diào),計(jì)算軟鍵盤高度,我們將軟鍵盤高度作為輸入框父容器的MarginBottom,就形成了輸入框被軟鍵盤彈起的情況。

  2. 由于這種情況很通用,所以可以抽取為一個(gè)通用的Callback。同時(shí)為Callback提供生命周期回調(diào)。

/**
 * 回調(diào)空實(shí)現(xiàn)
 */
public static class OnKeyboardChangeAdapter implements OnKeyboardChangeCallback {

    @Override
    public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
    }

    @Override
    public void onPause() {
    }

    @Override
    public void onDestroy() {
    }
}

/**
 * 存在輸入框場(chǎng)景的監(jiān)聽,鍵盤彈起時(shí),設(shè)置輸入框距離底部一個(gè)鍵盤高度,鍵盤下降時(shí)取消距離
 */
public static class CallbackToInput extends OnKeyboardChangeAdapter {
    /**
     * 輸入框容器
     */
    private View vInputContainer;

    public CallbackToInput(View inputContainer) {
        vInputContainer = inputContainer;
    }

    @Override
    public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
        super.onChange(isVisible, contentHeight, usableHeight);
        if (isVisible) {
            showInputView(usableHeight, contentHeight);
        } else {
            setBottomMargin(0);
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        //界面暫停時(shí),下降輸入框
        setBottomMargin(0);
    }

    /**
     * 顯示虛擬鍵盤時(shí)調(diào)用,顯示輸入框,如果綁定了列表控件則滾動(dòng)到后一個(gè)item
     *
     * @param height 用于計(jì)算虛擬鍵盤的高度
     */
    private void showInputView(int height, int contentHeight) {
        if (vInputContainer == null) {
            return;
        }
        //虛擬鍵盤的高度
        int keyboardHeight = contentHeight - height;
        setBottomMargin(keyboardHeight);
    }

    /**
     * 重新設(shè)置inputContainer的底部邊距
     */
    private void setBottomMargin(int bottomMargin) {
        if (vInputContainer == null) {
            return;
        }
        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) vInputContainer.getLayoutParams();
        if (params.bottomMargin != bottomMargin) {
            params.bottomMargin = bottomMargin;
            vInputContainer.requestLayout();
        }
    }
}
  1. 使用監(jiān)聽
mKeyboardFix.addOnKeyboardChangeListener(new KeyboardFix.CallbackToInput(inputContainer));

拓展KeyboardFix,鍵盤彈起,自動(dòng)滾動(dòng)RecyclerView

像QQ、微信,軟鍵盤彈起時(shí),滾動(dòng)列表控件到底部,由于我們已經(jīng)有了軟鍵盤回調(diào)了,所以處理起來就很簡(jiǎn)單了,一樣我們將這種通用的行為封裝為Callback即可。

/**
 * 存在列表場(chǎng)景的監(jiān)聽,一般鍵盤彈起時(shí),列表需要滾動(dòng)到底部,就可以添加該類型監(jiān)聽
 */
public static class CallbackToList extends OnKeyboardChangeAdapter {
    private RecyclerView vRecyclerView;
    private boolean mIsReverse;

    /**
     * 是否反轉(zhuǎn),反轉(zhuǎn)則滾動(dòng)到第0位,非反轉(zhuǎn)則滾動(dòng)到列表的最后1位
     *
     * @param isReverse true為反轉(zhuǎn)
     */
    public CallbackToList(RecyclerView recyclerView, boolean isReverse) {
        vRecyclerView = recyclerView;
        mIsReverse = isReverse;
    }

    @Override
    public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
        super.onChange(isVisible, contentHeight, usableHeight);
        //鍵盤彈起時(shí)滾動(dòng)到底部
        if (isVisible) {
            int position;
            if (mIsReverse) {
                position = 0;
            } else {
                if (vRecyclerView.getAdapter() == null) {
                    return;
                }
                position = vRecyclerView.getAdapter().getItemCount() - 1;
            }
            if (vRecyclerView != null && vRecyclerView.getAdapter() != null) {
                vRecyclerView.scrollToPosition(position);
            }
        }
    }
}
  • 使用監(jiān)聽
mKeyboardFix.addOnKeyboardChangeListener(new KeyboardFix.CallbackToList(recyclerView, false));
  • 分發(fā)生命周期事件

最后要記得在Activity或Fragment分發(fā)生命周期事件給KeyboardFix。

@Override
protected void onPause() {
    super.onPause;
    if (mKeyboardFix != null) {
        mKeyboardFix.onPause();
    }
}

@Override
protected void onDestroy() {
    super.onDestroy();
    if (mKeyboardFix != null) {
        mKeyboardFix.onDestroy();
    }
}

完整代碼

public class KeyboardFix implements ViewTreeObserver.OnGlobalLayoutListener {
    /**
     * 父容器
     */
    private View vRootView;
    /**
     * 父容器的高度
     */
    private int mContentHeight;
    /**
     * 標(biāo)記,第一次回調(diào)時(shí)保存父容器的高度
     */
    private boolean isFirst = true;
    /**
     * 最后一次顯示父容器的高度,用于判斷是否顯示虛擬鍵盤
     */
    private int mLastShowHeight;
    /**
     * 鍵盤監(jiān)聽
     */
    private List<OnKeyboardChangeCallback> mOnKeyboardChangeCallbacks;

    /**
     * 根容器
     */
    public KeyboardFix(View rootView) {
        mOnKeyboardChangeCallbacks = new CopyOnWriteArrayList<>();
        vRootView = rootView;
        if (rootView != null) {
            rootView.getViewTreeObserver().addOnGlobalLayoutListener(this);
        }
    }

    @Override
    public void onGlobalLayout() {
        possiblyResizeChildOfContent();
    }

    public void addOnKeyboardChangeListener(OnKeyboardChangeCallback onKeyboardChangeCallback) {
        mOnKeyboardChangeCallbacks.add(onKeyboardChangeCallback);
    }

    /**
     * 重新調(diào)整跟布局的高度
     */
    private void possiblyResizeChildOfContent() {
        //計(jì)算內(nèi)容的可見高度
        int usableHeightNow = computeUsableHeight();
        if (isFirst) {
            //兼容華為等機(jī)型
            mContentHeight = usableHeightNow;
            isFirst = false;
        }
        //沒有改變,忽略
        if (usableHeightNow == mLastShowHeight) {
            return;
        }
        mLastShowHeight = usableHeightNow;
        //判斷是否彈起軟鍵盤
        boolean isShowKeyboard = usableHeightNow + 200 < mContentHeight;
        for (OnKeyboardChangeCallback listener : mOnKeyboardChangeCallbacks) {
            listener.onChange(isShowKeyboard, mContentHeight, usableHeightNow);
        }
    }

    /**
     * 計(jì)算內(nèi)容的可見高度
     *
     * @return 父容器的可見高度
     */
    private int computeUsableHeight() {
        Rect rect = new Rect();
        vRootView.getWindowVisibleDisplayFrame(rect);
        return (rect.bottom - rect.top);
    }

    /**
     * 界面暫時(shí)時(shí)調(diào)用
     */
    public void onPause() {
        if (mOnKeyboardChangeCallbacks != null) {
            for (OnKeyboardChangeCallback callback : mOnKeyboardChangeCallbacks) {
                callback.onPause();
            }
        }
    }

    /**
     * 界面銷毀時(shí)調(diào)用,取消注冊(cè),防止內(nèi)存泄漏
     */
    public void onDestroy() {
        if (vRootView != null) {
            vRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            vRootView = null;
        }
        if (mOnKeyboardChangeCallbacks != null) {
            for (OnKeyboardChangeCallback callback : mOnKeyboardChangeCallbacks) {
                callback.onDestroy();
            }
            mOnKeyboardChangeCallbacks.clear();
            mOnKeyboardChangeCallbacks = null;
        }
    }

    /**
     * 虛擬鍵盤顯示隱藏的監(jiān)聽
     */
    public interface OnKeyboardChangeCallback {
        /**
         * 虛擬鍵盤顯示發(fā)生變化時(shí)調(diào)用
         *
         * @param isVisible     true為可見,false為不可見
         * @param contentHeight 原始內(nèi)容高度
         * @param usableHeight  當(dāng)前可用高度
         */
        void onChange(boolean isVisible, int contentHeight, int usableHeight);

        /**
         * 界面暫時(shí)回調(diào)
         */
        void onPause();

        /**
         * 界面銷毀時(shí)回調(diào)
         */
        void onDestroy();
    }

    /**
     * 回調(diào)空實(shí)現(xiàn)
     */
    public static class OnKeyboardChangeAdapter implements OnKeyboardChangeCallback {

        @Override
        public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
        }

        @Override
        public void onPause() {
        }

        @Override
        public void onDestroy() {
        }
    }

    /**
     * 顯示軟鍵盤
     */
    public void showSoftInput(final View view) {
        InputMethodManager manager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (manager == null) {
            return;
        }
        view.setFocusable(true);
        view.setFocusableInTouchMode(true);
        view.requestFocus();
        manager.showSoftInput(view, InputMethodManager.SHOW_FORCED);
    }

    /**
     * 存在輸入框場(chǎng)景的監(jiān)聽,鍵盤彈起時(shí),設(shè)置輸入框距離底部一個(gè)鍵盤高度,鍵盤下降時(shí)取消距離
     */
    public static class CallbackToInput extends OnKeyboardChangeAdapter {
        /**
         * 輸入框容器
         */
        private View vInputContainer;

        public CallbackToInput(View inputContainer) {
            vInputContainer = inputContainer;
        }

        @Override
        public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
            super.onChange(isVisible, contentHeight, usableHeight);
            if (isVisible) {
                showInputView(usableHeight, contentHeight);
            } else {
                setBottomMargin(0);
            }
        }

        @Override
        public void onPause() {
            super.onPause();
            //界面暫停時(shí),下降輸入框
            setBottomMargin(0);
        }

        /**
         * 顯示虛擬鍵盤時(shí)調(diào)用,顯示輸入框,如果綁定了列表控件則滾動(dòng)到后一個(gè)item
         *
         * @param height 用于計(jì)算虛擬鍵盤的高度
         */
        private void showInputView(int height, int contentHeight) {
            if (vInputContainer == null) {
                return;
            }
            //虛擬鍵盤的高度
            int keyboardHeight = contentHeight - height;
            setBottomMargin(keyboardHeight);
        }

        /**
         * 重新設(shè)置inputContainer的底部邊距
         */
        private void setBottomMargin(int bottomMargin) {
            if (vInputContainer == null) {
                return;
            }
            ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) vInputContainer.getLayoutParams();
            if (params.bottomMargin != bottomMargin) {
                params.bottomMargin = bottomMargin;
                vInputContainer.requestLayout();
            }
        }
    }

    /**
     * 存在列表場(chǎng)景的監(jiān)聽,一般鍵盤彈起時(shí),列表需要滾動(dòng)到底部,就可以添加該類型監(jiān)聽
     */
    public static class CallbackToList extends OnKeyboardChangeAdapter {
        private RecyclerView vRecyclerView;
        private boolean mIsReverse;

        /**
         * 是否反轉(zhuǎn),反轉(zhuǎn)則滾動(dòng)到第0位,非反轉(zhuǎn)則滾動(dòng)到列表的最后1位
         *
         * @param isReverse true為反轉(zhuǎn)
         */
        public CallbackToList(RecyclerView recyclerView, boolean isReverse) {
            vRecyclerView = recyclerView;
            mIsReverse = isReverse;
        }

        @Override
        public void onChange(boolean isVisible, int contentHeight, int usableHeight) {
            super.onChange(isVisible, contentHeight, usableHeight);
            //鍵盤彈起時(shí)滾動(dòng)到底部
            if (isVisible) {
                int position;
                if (mIsReverse) {
                    position = 0;
                } else {
                    if (vRecyclerView.getAdapter() == null) {
                        return;
                    }
                    position = vRecyclerView.getAdapter().getItemCount() - 1;
                }
                if (vRecyclerView != null && vRecyclerView.getAdapter() != null) {
                    vRecyclerView.scrollToPosition(position);
                }
            }
        }
    }
}

總結(jié)

雖然谷歌沒有提供鍵盤監(jiān)聽,但是我們可以曲線救國(guó),當(dāng)然希望官方可以修復(fù)這個(gè)bug,并且提供回調(diào)為最好,本篇作為記錄而寫。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容