在開發(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
就可以了辟癌,非常方便寒屯。