聊天輸入框跳閃的解決方案

在開發(fā)IM(即時(shí)聊天通訊)中不可避免要設(shè)計(jì)一些聊天窗口頁面,在輸入框惩歉、表情按鈕以及焦點(diǎn)切換時(shí)手機(jī)界面會不可避免會碰到一些非常僵硬的閃動問題啄栓,而這些在iOS據(jù)說自帶平滑過渡,而Android卻沒有這些優(yōu)化季蚂,而且經(jīng)筆者測試第三方IM的Demo都沒怎么優(yōu)化,所以只能我們自己動手來啦~

直接看代碼?

在聊天界面我們一般會分成若干個(gè)層級扭屁,頂部區(qū)域(聊天者的姓名)算谈,內(nèi)容區(qū)域(聊天記錄),底部區(qū)域(輸入框料滥,按鈕然眼,Emoji面板等)。
我們簡單來設(shè)計(jì)一個(gè)界面葵腹,代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <android.support.v4.widget.SwipeRefreshLayout
            android:id="@+id/swipe_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/recyclerview"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </android.support.v4.widget.SwipeRefreshLayout>
    </RelativeLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:background="@android:color/darker_gray" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:gravity="center_vertical"
            android:orientation="horizontal"
            android:paddingLeft="10dp"
            android:paddingRight="10dp">

            <EditText
                android:id="@+id/et_content"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1" />

            <CheckBox
                android:id="@+id/cbx_emoji"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Emoji" />

            <Button
                android:id="@+id/btn_send"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="發(fā)送" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/pannel"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:gravity="center"
            android:visibility="gone">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="模擬表情面板" />
        </LinearLayout>
    </LinearLayout>

</LinearLayout>

運(yùn)行起來是這個(gè)樣子


我們給他加上一些必要的監(jiān)聽事件高每,就別再在意界面太丑了(;?_?)

        rootLayout = (MeasureLinearLayout) findViewById(R.id.root_layout);
        swipeLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_layout);
        recyclerview = (RecyclerView) findViewById(R.id.recyclerview);
        etContent = (EditText) findViewById(R.id.et_content);
        cbxEmoji = (CheckBox) findViewById(R.id.cbx_emoji);
        btnSend = (Button) findViewById(R.id.btn_send);
        pannel = (LinearLayout) findViewById(R.id.pannel);

        recyclerview.setLayoutManager(new LinearLayoutManager(this));
        recyclerview.setHasFixedSize(true);
        recyclerview.setAdapter(new TestAdapter());
        cbxEmoji.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (cbxEmoji.isChecked()) {
                    //顯示Emoji面板
                    pannel.setVisibility(View.VISIBLE);
                    etContent.clearFocus();
                    KeyBoardUtils.hideKeyboard(etContent);
                } else {
                    //隱藏
                    pannel.setVisibility(View.GONE);
                    etContent.requestFocus();
                    KeyBoardUtils.showKeyboard(etContent);
                }
            }
        });
        recyclerview.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_DOWN) {
                    //點(diǎn)擊列表部分就清除焦點(diǎn)并初始化狀態(tài)
                    if (pannel.getVisibility() == View.VISIBLE) {
                        pannel.setVisibility(View.GONE);
                        cbxEmoji.setChecked(false);
                    } else {
                        etContent.clearFocus();
                        KeyBoardUtils.hideKeyboard(etContent);
                    }
                }
                return false;
            }
        });
        swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                swipeLayout.setRefreshing(false);
            }
        });
        etContent.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_DOWN && !etContent.isFocused()) {
                    //觸摸輸入框時(shí)彈出輸入法
                    cbxEmoji.setChecked(false);
                    pannel.setVisibility(View.GONE);
                    KeyBoardUtils.showKeyboard(etContent);
                }
                return false;
            }
        });

代碼很簡單,我們跑起來看看效果


轉(zhuǎn)換出來的Gif幀數(shù)比較低践宴,不過我們也可以看得更仔細(xì)鲸匿,軟鍵盤顯示和隱藏都不和我們的pannel面板同步,每次都慢一拍顯得十分突兀阻肩,那么我們就來手動讓它們同步褂痰。
我們可以發(fā)現(xiàn)界面被軟鍵盤擠壓的時(shí)候界面的高度也被迫發(fā)生了變化碟绑,那么這個(gè)就是我們的切入口咏尝,我們簡單的自定義一個(gè)控件

public class MeasureLinearLayout extends LinearLayout {
    public MeasureLinearLayout(Context context) {
        super(context);
    }

    public MeasureLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MeasureLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.e("width", MeasureSpec.getSize(widthMeasureSpec) + "");
        Log.e("height", MeasureSpec.getSize(heightMeasureSpec) + "");
    }
}

再測量的時(shí)候把測量信息輸出跑慕,我們把他代替我們之前Activity的布局下的根節(jié)點(diǎn)LinearLayout,我們在來觸發(fā)一次擠壓界面:

03-30 19:03:08.050 12117-12117/com.tk.iminputdemo E/width: 1080
03-30 19:03:08.050 12117-12117/com.tk.iminputdemo E/height: 1677
03-30 19:03:08.050 12117-12117/com.tk.iminputdemo E/width: 1080
03-30 19:03:08.050 12117-12117/com.tk.iminputdemo E/height: 776

我們可以發(fā)現(xiàn)當(dāng)要顯示軟鍵盤的時(shí)候第一次的時(shí)候測量出的值就是控件原有的值撕氧,第二次測出來就是被擠壓后的值瘤缩,我們之前在Activity中寫的邏輯都在onMeasure出結(jié)果以后才執(zhí)行,那么我們再得知結(jié)果以前呢伦泥?
我們新建一個(gè)類KeyBoardObservable剥啤,在我們自定義的布局里面監(jiān)聽onMeasure結(jié)果,

    public MeasureLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        keyBoardObservable = new KeyBoardObservable();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        keyBoardObservable.beforeMeasure(heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
public class KeyBoardObservable {
    private static final String TAG = "KeyBoardObservable";

    private int lastHeight;
    private List<KeyBoardObserver> observers;

    private boolean keyBoardVisibile;

    /**注冊監(jiān)聽
     * 
     * @param listener
     */
    public void register(@NonNull KeyBoardObserver listener) {
        if (observers == null) {
            observers = new ArrayList<>();
        }
        observers.add(listener);
    }
    /**搶先測量
     * 
     * @param heightMeasureSpec
     */
    public void beforeMeasure(int heightMeasureSpec) {
        int height = View.MeasureSpec.getSize(heightMeasureSpec);
        Log.d(TAG, "height: " + height);
        if (lastHeight == 0) {
            lastHeight = height;
            return;
        }
        if (lastHeight == height) {
            //沒有發(fā)生擠壓
            return;
        }
        final int offset = lastHeight - height;
        if (Math.abs(offset) < DensityUtil.dp2px(80)) {
            //80 dp 擠壓閾值
            return;
        }
        if (offset > 0) {
            Log.d(TAG, "軟鍵盤顯示了");
            keyBoardVisibile = true;
        } else {
            Log.d(TAG, "軟鍵盤隱藏了");
            keyBoardVisibile = false;
        }
        update(keyBoardVisibile);
        lastHeight = height;
    }

    public boolean isKeyBoardVisibile() {
        return keyBoardVisibile;
    }

    /**
     * 通知更新
     * @param keyBoardVisibile
     */
    private void update(final boolean keyBoardVisibile) {
        if (observers != null) {
            for (KeyBoardObserver observable : observers) {
                observable.update(keyBoardVisibile);
            }
        }
    }
}
public interface KeyBoardObserver {
    void update(boolean keyBoardVisibile);
}

代碼不多不脯,很簡單采用了觀察者模式來設(shè)計(jì)府怯,因?yàn)槲覀冇眠@種方式相當(dāng)于可以注冊了軟鍵盤的狀態(tài)(不知道Android API什么時(shí)候會有)。如果2次測量結(jié)果沒有發(fā)生變化或者小于閾值(隨便設(shè)的防楷,防止一些偶然性的界面變化)牺丙,那么我們Activity的業(yè)務(wù)邏輯就要改了,因?yàn)閺纳厦娴膶?shí)驗(yàn)結(jié)果我們看出來setVisibile的調(diào)用時(shí)機(jī)不能簡單的和顯示/隱藏軟鍵盤一塊隨便出沒复局。
我們先來處理開啟表情面板的CheckBox冲簿,邏輯如下:

                if (cbxEmoji.isChecked()) {
                    //想要顯示面板
                    etContent.clearFocus();
                    if (rootLayout.getKeyBoardObservable().isKeyBoardVisibile()) {
                        //當(dāng)前軟鍵盤為 掛起狀態(tài)
                        //隱藏軟鍵盤并顯示面板
                        KeyBoardUtils.hideKeyboard(etContent);
                    } else {
                        //顯示面板
                        pannel.setVisibility(View.VISIBLE);
                    }
                } else {
                    //想要關(guān)閉面板
                    //掛起軟鍵盤,并隱藏面板
                    etContent.requestFocus();
                    KeyBoardUtils.showKeyboard(etContent);
                }

分以下幾個(gè)點(diǎn):

  • 當(dāng)我們要顯示面板的時(shí)候要判斷當(dāng)前的狀態(tài)亿昏,如果是軟鍵盤掛起的狀態(tài)(焦點(diǎn)在EditText上)峦剔,那此時(shí)要平滑的讓軟鍵盤隱藏,并且顯示面板
  • 如果軟鍵盤未掛起角钩,也就是初始化狀態(tài)吝沫,那就直接顯示面板了
  • 想要關(guān)閉面板的時(shí)候呻澜,那此時(shí)要平滑的讓軟鍵盤顯示,并且隱藏面板

以上都是筆者在使用微信時(shí)得出的簡化版業(yè)務(wù)邏輯
然后我們在Activity中注冊監(jiān)聽:

rootLayout.getKeyBoardObservable().register(this);
    @Override
    public void update(boolean keyBoardVisibile) {
        if (keyBoardVisibile) {
            //軟鍵盤掛起
            pannel.setVisibility(View.GONE);
            cbxEmoji.setChecked(false);

        } else {
            //回復(fù)原樣
            if (cbxEmoji.isChecked()) {
                pannel.setVisibility(View.VISIBLE);
            }
        }
    }

在接受到軟鍵盤觀察者的信息后惨险,如果當(dāng)前軟鍵盤為掛起狀態(tài)我們就把面板隱藏羹幸,如果軟鍵盤要變?yōu)殡[藏狀態(tài),且此時(shí)是需要顯示面板的辫愉,就平滑顯示面板栅受。
由于我們此時(shí)已經(jīng)能監(jiān)聽到軟鍵盤的狀態(tài)了,那EditText的Touch事件就沒必要監(jiān)聽了一屋。
我們再來運(yùn)行下看看效果:


果然同步了,仿佛像日落下風(fēng)平浪靜的港灣一般袋哼,那么的令人陶醉
(??????)??
但是好像我們還是觸礁了(°ー°〃)冀墨,由于面板的高度和軟鍵盤的高度不一致,還是不和諧的地方涛贯,這然身為強(qiáng)迫癥的筆者怎么能忍诽嘉?
由于Android中并沒有常規(guī)API來獲取軟鍵盤高度,更何況我們已經(jīng)用這種方式能監(jiān)聽到軟鍵盤的狀態(tài)了弟翘,獲取高度簡直就是信手拈來虫腋,我們建一個(gè)工具類用來保存軟鍵盤的數(shù)據(jù):

public class SharePrefenceUtils {
    public static final String KEYBOARD = "keyboard";
    public static final String HEIGHT = "height";

    /**
     * 保存軟鍵盤高度
     *
     * @param context
     * @param height
     */
    public static void saveKeyBoardHeight(@NonNull Context context, int height) {
        context.getSharedPreferences(KEYBOARD, Context.MODE_PRIVATE).edit().putInt(HEIGHT, height).commit();
    }

    /**
     * 獲取軟鍵盤高度,默認(rèn)為界面一半高度
     *
     * @param context
     * @return
     */
    public static int getKeyBoardHeight(@NonNull Context context) {
        int defaultHeight = context.getResources().getDisplayMetrics().heightPixels >> 1;
        return context.getSharedPreferences(KEYBOARD, Context.MODE_PRIVATE).getInt(HEIGHT, defaultHeight);
    }
}

在我們的軟鍵盤觀察者的方法也要相應(yīng)的修改

 public void beforeMeasure(Context context, int heightMeasureSpec) {
         //上文的代碼稀余,這里就省略了
        int keyBoardHeight = Math.abs(offset);
        SharePrefenceUtils.saveKeyBoardHeight(context, keyBoardHeight);
        update(keyBoardVisibile, keyBoardHeight);
        lastHeight = height;
    }

我們需要一個(gè)上下文Context悦冀,界面被擠壓的形變高度也就是我們要獲取軟鍵盤的高度了。
在Activity中我們也要相應(yīng)的修改

pannel = (LinearLayout) findViewById(R.id.pannel);
//初始化高度睛琳,和軟鍵盤一致盒蟆,初值為手機(jī)高度一半
pannel.getLayoutParams().height = SharePrefenceUtils.getKeyBoardHeight(this);
    @Override
    public void update(boolean keyBoardVisibile, int keyBoardHeight) {
        if (keyBoardVisibile) {
            //軟鍵盤掛起
            pannel.setVisibility(View.GONE);
            cbxEmoji.setChecked(false);

        } else {
            //回復(fù)原樣
            if (cbxEmoji.isChecked()) {
                if (pannel.getLayoutParams().height != keyBoardHeight) {
                    pannel.getLayoutParams().height = keyBoardHeight;
                }
                pannel.setVisibility(View.VISIBLE);
            }
        }
    }

我們最后來看下效果


OK,跳閃問題我們就這樣解決了师骗,這是一種監(jiān)聽根布局?jǐn)D壓历等,在重新onMeasure時(shí)手動是軟鍵盤和界面刷新同步的方案,在項(xiàng)目有需要時(shí)我們只要繼承別的布局重寫下onMeasure就可以了辟癌,非常方便寒屯。

完整代碼戳這里

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市黍少,隨后出現(xiàn)的幾起案子寡夹,更是在濱河造成了極大的恐慌,老刑警劉巖厂置,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件要出,死亡現(xiàn)場離奇詭異,居然都是意外死亡农渊,警方通過查閱死者的電腦和手機(jī)患蹂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門或颊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人传于,你說我怎么就攤上這事囱挑。” “怎么了沼溜?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵平挑,是天一觀的道長。 經(jīng)常有香客問我系草,道長通熄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任找都,我火速辦了婚禮唇辨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘能耻。我一直安慰自己赏枚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布晓猛。 她就那樣靜靜地躺著饿幅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪戒职。 梳的紋絲不亂的頭發(fā)上栗恩,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天,我揣著相機(jī)與錄音洪燥,去河邊找鬼摄凡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蚓曼,可吹牛的內(nèi)容都是我干的亲澡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼纫版,長吁一口氣:“原來是場噩夢啊……” “哼床绪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起其弊,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤癞己,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后梭伐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痹雅,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年糊识,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绩社。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摔蓝。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖愉耙,靈堂內(nèi)的尸體忽然破棺而出贮尉,到底是詐尸還是另有隱情,我是刑警寧澤朴沿,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布猜谚,位于F島的核電站,受9級特大地震影響赌渣,放射性物質(zhì)發(fā)生泄漏魏铅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一坚芜、第九天 我趴在偏房一處隱蔽的房頂上張望览芳。 院中可真熱鬧,春花似錦货岭、人聲如沸路操。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至搞坝,卻和暖如春搔谴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背桩撮。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工敦第, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人店量。 一個(gè)月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓芜果,卻偏偏與公主長得像,于是被迫代替她去往敵國和親融师。 傳聞我的和親對象是個(gè)殘疾皇子右钾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容