开源项目之 PanelSwitchHelper(二)

PanelSwitchHelper 框架已经有一段时间了,功能的稳定性,系统版本及机型兼容性也没问题。随着业界的交互做得越来越好,issue 上有朋友建议能够增强框架的交互,在切换面板/输入法的时候能更为平滑地过渡。

大家还希望 PanelSwitchHelper 新增哪些功能?欢迎大家盖楼 issue可见建议。

在 version 1.1.0 之前,PanewitchHelper 的实现大致为:

  • 通过监听 window 变化计算输入法的高度;
  • 设置框架根布局为线型布局且动态更改 weight 值,在输入法隐藏和显示前夕, weight 设置为1;切换功能面本是 weigth 设置为0且高度固定。

由于 weight 的值是线型布局测量时使用 weightSum 进行等分计算,window 变化之后,线型布局自然重新计算绘制,每一次 layou 布局时会获取当前系统测量后的具体值。切换输入法时,layout 时机必定在 window 变化之后,所以 ContentContainer 的变化肯定稍慢于 window 的变化,仔细观看会发现有些卡顿。切换面板时,由于上层 ContentContainer 高度固定,所以 PanelContainer 区域就会可见,这样切换虽然界面不会跳动,但是会生硬。

尝试过使用属性动画来动态更改 ContentContainer 的高度,但是 PanelContainer 切换依然会卡顿。原因是 “我试图从 contentContainer 和 panelContainer 独立的部分” 进行动画过度。后面发现从根本上思路错了。要实现 “微信切换面板” 的效果,应该把两者看成一个整体。

“你可以看到,微信在切换输入法的时候很平滑。”

“你可以看到,微信在切换功能面板的时候,好像是面板一开始就已经在屏幕底部下面,然后被推上去一样。”

所以在显示和隐藏的时候,直接干预 ContentContainerPanelContainer 父布局 P
anelSwitchLayout
的绘制。假设这里我们暂时忽略状态栏和导航栏,且 PanelSwitchLayout 铺满界面场景,H(view) 表示 view 的高度

  • 界面的高度 H(panelSwitchLayout)
  • contentContainer 的高度 H(contentContainer)
  • panelContainer 的高度 H(panelContainer)
  • 输入法高度 H(Keyboard)

框架需要考虑的场景都 3 个

  • 输入法隐藏面板隐藏
  • 输入法隐藏面板显示
  • 输入法显示面板显示

且满足

  • H(panelSwitchLayout) = H(contentContainer)
  • H(panelContainer) = H(Keyboard)

这里我们要利用 ChangeBounds 的动画实现来帮助 PanelSwitchLayout 在 Bound 变化的时候做动画过度,那么我们就得从 3 个场景下完成正确的边缘计算。满足如下:“panelSwitchLayout 在 onLayout 的时候,需要从上往下一次 layout contentContainer 和 panelContainer。” view.top 表示 view 在其 parent 内 layout时的 top 值。

  • 输入法隐藏面板隐藏,contentContainer.top = 0
  • 输入法隐藏面板显示,contentContainer.top = -H(panelContainer)
  • 输入法显示面板隐藏,contentContainer.top = -H(panelContainer)

“那么,输入法隐藏面板隐藏场景下,我们就能看到整个界面都是 ContentContainer,面板/输入法显示的时候,整体上移,非常连贯。”

但实际上我们还需要计算状态栏,导航栏,标题栏等视图元素,还需要考虑 PanelSwitchLayout 不是当前界面的根布局场景。这部分计算非常痛苦,要考虑的东西非常多。 同时还要考虑界面是否支持沉浸,用户机型会不会存在动态隐藏导航等等(此时心里有百万个草泥马蹦腾)。尝试了一整天,每一种方案都可能在另一些特殊场景中不适用。

在隔天早上去公司的路上,感觉有了能兼容所有场景的灵感,可结合以下条件综合计算。

  • 计算除了导航栏高度之外的界面所有高度 h1;
  • 计算 panelSwitchLayout 在界面的绝对 y 值(也就是绘制起点距离顶部的高度);
  • h1 - y - panelSwitchLayout.paddingTop 可以得到 panelSwitchLayout 该有的高度 h2;
  • 如果设备有导航栏且不可见,则 h2 需要加上导航栏高度。

最后计算的代码逻辑如下。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
    /**
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int visibility = getVisibility();
if (visibility != VISIBLE) {
return;
}

int screenHeight = PanelHelper.getScreenHeightWithSystemUI(window);
// int screenWithoutSystemUIHeight = PanelHelper.getScreenHeightWithoutSystemUI(window);
int screenWithoutNavigationIHeight = PanelHelper.getScreenHeightWithoutNavigationBar(getContext());
// int systemUIHeight = PanelHelper.getSystemUI(getContext(), window);
// int statusBarHeight = PanelHelper.getStatusBarHeight(getContext());
int navigationBarHeight = PanelHelper.getNavigationBarHeight(getContext());
boolean navigationBarShow = PanelHelper.isNavigationBarShow(getContext(), window);
//以这种方式计算出来的toolbar,如果和statusBarHeight一样,则实际上就是statusBar的高度,大于statusBar的才是toolBar的高度。
// int toolbarHeight = PanelHelper.getToolbarHeight(window);
// if (toolbarHeight == statusBarHeight) {
// toolbarHeight = 0;
// }
// int contentViewHeight = PanelHelper.getContentViewHeight(window);


int keyboardHeight = PanelHelper.getKeyBoardHeight(getContext());
int paddingTop = getPaddingTop();
int allHeight = screenWithoutNavigationIHeight;
if (PanelHelper.isPortrait(getContext())) {

//兼容性测试中,国产手机支持完全隐藏导航栏或者动态隐藏显示导航栏。前者往往使用实键或者手势来控制页面的返回。针对前者,screenHeight是会等于screenWithoutNavigationHeight,后者则一直不相等
//为了实时使布局响应界面导航栏引起的变化,需要在隐藏导航栏的时候,把这部分高度归还给我们的界面
if (screenHeight != screenWithoutNavigationIHeight) {
allHeight += navigationBarShow ? 0 : navigationBarHeight;
}

}
int[] localLocation = PanelHelper.getLocationOnScreen(this);
allHeight -= localLocation[1];


int contentContainerTop = (panelId == Constants.PANEL_NONE) ? 0 : -keyboardHeight;
contentContainerTop += paddingTop;


int contentContainerHeight = allHeight - paddingTop;
int panelContainerTop = contentContainerTop + contentContainerHeight;
int panelContainerHeight = keyboardHeight;

setTransition(animationSpeed);

// Log.d(TAG, " ");
// Log.d(TAG, " onLayout =======> 被回调 ");
// Log.d(TAG, " layout参数 changed : " + changed + " l : " + l + " t : " + t + " r : " + r + " b : " + b);
// Log.d(TAG, " panel场景 : " + (panelId == Constants.PANEL_NONE ? "收起" : (panelId == Constants.PANEL_KEYBOARD ? "键盘" : "面板")));
//
// Log.d(TAG, " 界面高度(包含系统UI) :" + screenHeight);
// Log.d(TAG, " 界面高度(不包含导航栏) :" + screenWithoutNavigationIHeight);
// Log.d(TAG, " 内容高度(不包含系统UI) :" + screenWithoutSystemUIHeight);
// Log.d(TAG, " 系统UI高度 :" + systemUIHeight);
// Log.d(TAG, " 系统状态栏高度 :" + statusBarHeight);
// Log.d(TAG, " 系统导航栏高度 :" + navigationBarHeight);
// Log.d(TAG, " 系统导航栏是否显示 :" + navigationBarShow);
// Log.d(TAG, " contentView高度 :" + contentViewHeight);
// Log.d(TAG, " switchLayout 绘制起点 :(" + localLocation[0] + "," + localLocation[1] + ")");
// Log.d(TAG, " toolbar高度 :" + toolbarHeight);
// Log.d(TAG, " paddingTop :" + paddingTop);
// Log.d(TAG, " 输入法高度 :" + keyboardHeight);
// Log.d(TAG, " 内容 top :" + contentContainerTop);
// Log.d(TAG, " 内容 高度 :" + contentContainerHeight);
// Log.d(TAG, " 面板 top :" + panelContainerTop);
// Log.d(TAG, " 面板 高度 " + panelContainerHeight);

//处理第一个view contentContainer
{
contentContainer.layout(l, contentContainerTop, r, contentContainerTop + contentContainerHeight);
Log.d(TAG, " layout参数 contentContainer : height - " + contentContainerHeight);
Log.d(TAG, " layout参数 contentContainer : " + " l : " + l + " t : " + contentContainerTop + " r : " + r + " b : " + (contentContainerTop + contentContainerHeight));
ViewGroup.LayoutParams layoutParams = contentContainer.getLayoutParams();
if (layoutParams.height != contentContainerHeight) {
layoutParams.height = contentContainerHeight;
contentContainer.setLayoutParams(layoutParams);
}
}

//处理第二个view panelContainer
{
panelContainer.layout(l, panelContainerTop, r, panelContainerTop + panelContainerHeight);
Log.d(TAG, " layout参数 panelContainerTop : height - " + panelContainerHeight);
Log.d(TAG, " layout参数 panelContainer : " + " l : " + l + " : " + panelContainerTop + " r : " + r + " b : " + (panelContainerTop + panelContainerHeight));
ViewGroup.LayoutParams layoutParams = panelContainer.getLayoutParams();
if (layoutParams.height != panelContainerHeight) {
layoutParams.height = panelContainerHeight;
panelContainer.setLayoutParams(layoutParams);
}
}
}

感兴趣可以参考 PanelSwitchLayout 类实现。

路漫漫其修远兮,只要有心实现就一定可以!