这是一位微信出来的哥们儿写的开源项目,解决表情 Panel 和键盘冲突的问题。本人之前做布丁动画时一直有这个困扰,看到这个项目,真是有种春风拂过泸沽湖的感觉。
项目地址在这里 https://github.com/Jacksgong/JKeyboardPanelSwitch
在分析原理前,先看一下为什么我们要使用 JKeyboardPanelSwitch,反之就是,如果不使用 JKeyboardPanelSwitch,我们会遇到哪些问题。
假设我们现在要自己实现一个如 Demo 所示的页面,我们会如何做呢?
计算键盘高度,根据键盘高度设置 Panel 高度;
监听键盘,当键盘显示时,如果 Panel 可见,则隐藏;
解决 Panel 可见情况下,键盘突然弹起,Panel 隐藏不及时的情况;
上面是我列出的可能遇到的问题,所以理想情况下 JKeyboardPanelSwitch 应该包含上面的 4 种解决方案。
然后我们带着问题,去看源码。我喜欢从入口开始,也就是 Demo 里面的 Activity.
首先是布局的 xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <cn.dreamtobe.kpswitch.widget.KPSwitchRootLinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:id ="@+id/rootView" android:layout_width ="match_parent" android:layout_height ="match_parent" android:orientation ="vertical" > <include layout ="@layout/view_message_list" /> <include layout ="@layout/layout_send_message_bar" /> <cn.dreamtobe.kpswitch.widget.KPSwitchPanelLinearLayout android:id ="@+id/panel_root" style ="@style/Panel" android:visibility ="gone" > <include layout ="@layout/merge_panel_content" /> </cn.dreamtobe.kpswitch.widget.KPSwitchPanelLinearLayout > </cn.dreamtobe.kpswitch.widget.KPSwitchRootLinearLayout >
很明显,布局使用了两个定制的 LinearLayout,KPSwitchRootLinearLayout 和 KPSwitchPanelLinearLayout. KPSwitchRootLinearLayout 是整个页面的布局,KPSwitchPanelLinearLayout 则是 Panel 的布局。
首先看 KPSwitchRootLinearLayout, 继承了 LinearLayout,在 onMeasure 稍有处理。
1 2 3 4 5 protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { conflictHandler.handleBeforeMeasure(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)); super .onMeasure(widthMeasureSpec, heightMeasureSpec); }
每次 measure 都会调用 conflictHandler.handleBeforeMeasure(int, int)
, 看 ConflictHandler (部分精简).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public void handleBeforeMeasure (final int width, int height) { if (height < 0 ) { return ; } if (mOldHeight < 0 ) { mOldHeight = height; return ; } final int offset = mOldHeight - height; if (offset == 0 ) { return ; } if (Math.abs(offset) == mStatusBarHeight) { return ; } mOldHeight = height; final IPanelConflictLayout panel = getPanelLayout(mTargetRootView); if (offset > 0 ) { panel.handleHide(); } else if (panel.isKeyboardShowing()) { if (panel.isVisible()) { panel.handleShow(); } } }
界面生成就会第一次 measure,此时 mOldHeight < 0,所以结束后,mOldHeight = height; 之后每次显示 / 隐藏键盘都会再次触发 measure,计算 offset,更新 mOldHeight,通知 Panel 处理.
然后再看 KPSwitchPanelLinearLayout, 它实现了 IPanelConflictLayout 接口。
1 2 3 4 5 6 7 8 9 @Override public void handleShow () { super .setVisibility(View.VISIBLE); } @Override public void handleHide () { panelLayoutHandler.handleHide(); }
KPSwitchPanelLayoutHandler,
1 2 3 public void handleHide () { this .mIsHide = true ; }
可见仅仅是进行了一个标记。
接下来继续看 KPSwitchPanelLinearLayout,它的 onMeasure 调用了 panelLayoutHandler.processOnMeasure(int ,int)
1 2 3 4 5 6 protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) { final int [] processedMeasureWHSpec = panelLayoutHandler.processOnMeasure(widthMeasureSpec, heightMeasureSpec); super .onMeasure(processedMeasureWHSpec[0 ], processedMeasureWHSpec[1 ]); }
看 KPSwitchPanelLayoutHandler,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public int [] processOnMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mIsHide) { panelLayout.setVisibility(View.GONE); * The current frame will be visible nil. */ widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(0 , View.MeasureSpec.EXACTLY); heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0 , View.MeasureSpec.EXACTLY); } processedMeasureWHSpec[0 ] = widthMeasureSpec; processedMeasureWHSpec[1 ] = heightMeasureSpec; return processedMeasureWHSpec; }
也就是说,在 Panel 的 measure 过程中,如果发现 mIsHide 为 true,则设置 width = height = 0, 否则会正常显示 Panel.
至此我们发现,整套逻辑走完了,是在处理我们之前列出的几个问题中的第 3 点.
剩下的几点其实都就比较简单,在 Activity 的入口直接就可以找到,主要是通过设置一个 OnGlobalLayoutListener 来监听窗体大小变化来推算键盘高度,这就不细说了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); ... KeyboardUtil.attach(this , mPanelRoot, new KeyboardUtil.OnKeyboardShowingListener() { @Override public void onKeyboardShowing (boolean isShowing) { Log.d(TAG, String.format("Keyboard is %s" , isShowing ? "showing" : "hiding" )); } }); KPSwitchConflictUtil.attach(mPanelRoot, mPlusIv, mSendEdt, new KPSwitchConflictUtil.SwitchClickListener() { @Override public void onClickSwitch (boolean switchToPanel) { if (switchToPanel) { mSendEdt.clearFocus(); } else { mSendEdt.requestFocus(); } } }); ... }
KeyboardUtil.attach(),
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void attach (final Activity activity, IPanelHeightTarget target, OnKeyboardShowingListener listener) { final ViewGroup contentView = (ViewGroup) activity.findViewById(android.R.id.content); final boolean isFullScreen = ViewUtil.isFullScreen(activity); final boolean isTranslucentStatus = ViewUtil.isTranslucentStatus(activity); final boolean isFitSystemWindows = ViewUtil.isFitsSystemWindows(activity); contentView.getViewTreeObserver(). addOnGlobalLayoutListener( new KeyboardStatusListener(isFullScreen, isTranslucentStatus, isFitSystemWindows, contentView, target, listener)); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Override public void onGlobalLayout () { final View userRootView = contentView.getChildAt(0 ); final View contentParentView = (View) contentView.getParent(); Rect r = new Rect(); final int displayHeight; if (isTranslucentStatus) { contentParentView.getWindowVisibleDisplayFrame(r); displayHeight = (r.bottom - r.top) + statusBarHeight; } else { userRootView.getWindowVisibleDisplayFrame(r); displayHeight = (r.bottom - r.top); } calculateKeyboardHeight(displayHeight); calculateKeyboardShowing(displayHeight); previousDisplayHeight = displayHeight; }