5、Hotseat与AllApps:Hotseat的固定区域设计、AllAppsContainerView的列表实现、搜索框与预测应用
好,咱们今天聊聊桌面系统里两个最核心的入口——Hotseat和AllApps。说白了,一个是你最常用的应用快捷区,另一个是应用抽屉。这两个组件,几乎每个用户每天都要跟它们打交道几十次。我当年刚接手Launcher开发时,第一个改的就是Hotseat的布局逻辑,踩了不少坑,今天一并分享给你。
5.1 Hotseat的固定区域设计
Hotseat,就是桌面底部那排固定的图标区域。它跟普通工作区最大的区别是什么?——固定。不管用户怎么滑动桌面页面,Hotseat始终在那。嗯,这里要注意,它并不是简单的“多放一行图标”,它的布局逻辑、触摸响应、拖拽交互,都是独立于工作区的。
核心要点:Hotseat的CellLayout与Workspace的CellLayout共享同一套布局引擎,但Hotseat的容器是独立管理的。它的行数固定为1,列数由配置决定(通常是5或7)。
我个人习惯把Hotseat的布局参数放在一个单独的XML里,比如hotseat.xml,这样便于维护。它的根布局是一个Hotseat类,继承自FrameLayout,内部包含一个CellLayout。
<!-- hotseat.xml -->
<com.android.launcher3.Hotseat
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/hotseat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom">
<com.android.launcher3.CellLayout
android:id="@+id/hotseat_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:cellWidth="@dimen/hotseat_cell_width"
android:cellHeight="@dimen/hotseat_cell_height"
android:numColumns="@integer/hotseat_cell_count_x"
android:numRows="1" />
</com.android.launcher3.Hotseat>
这里有个细节:android:numRows="1"。为什么是1?因为Hotseat就是一行。但有些平板设备或横屏模式下,Hotseat可能会变成两行。我曾经在适配平板时,就因为这个写死了行数,导致布局错乱。后来我改成从配置文件中读取,问题就解决了。
避坑指南:Hotseat的图标大小不要跟工作区完全一样。通常Hotseat的图标会稍微大一点,因为它在底部,用户拇指更容易点击。我一般把Hotseat的图标尺寸设为工作区的1.1倍。
5.2 AllAppsContainerView的列表实现
AllAppsContainerView,就是应用抽屉的容器。它负责展示所有已安装的应用列表。这个组件在Launcher3里是一个RecyclerView的封装,支持快速滚动、字母索引、搜索过滤等功能。
它的核心结构是这样的:
- AllAppsContainerView:最外层容器,继承自
FrameLayout,管理搜索框和列表的布局。 - AllAppsRecyclerView:继承自
RecyclerView,负责应用列表的展示和交互。 - Adapter:
AllAppsListAdapter,绑定应用数据。 - Model:
AllAppsList,维护应用列表的数据模型。
你想想看,当用户打开应用抽屉时,系统需要做两件事:一是加载所有应用,二是按字母排序。这个排序逻辑,我建议放在后台线程做,否则在主线程上处理几百个应用,UI会明显卡顿。
// AllAppsList.java 核心代码片段
public void add(AppInfo app, AppFilter appFilter) {
if (appFilter != null && !appFilter.shouldShowApp(app)) {
return; // 过滤掉系统应用或隐藏应用
}
int index = Collections.binarySearch(mApps, app, AppInfo.TITLE_COMPARATOR);
if (index < 0) {
index = -(index + 1);
}
mApps.add(index, app);
mComponentToAppMap.put(app.componentName, app);
}
这段代码里,Collections.binarySearch保证了每次插入都是有序的。但注意,如果应用数量很大(比如超过500个),频繁的二分插入也会有一定开销。我之前的项目里,用户装了300多个应用,打开抽屉时明显感觉慢。后来我改成批量插入后统一排序,性能提升了不少。
注意:AllAppsRecyclerView的LayoutManager建议使用AlphabeticalFastScrollRecyclerView,它支持字母快速索引。但要注意,这个类在Launcher3里是隐藏的,你需要从源码中拷贝出来。
5.3 搜索框与预测应用
搜索框是AllAppsContainerView的标配。用户输入关键字时,列表会实时过滤。这个功能看起来简单,但实现起来有几个坑。
首先,搜索框的输入监听。我建议使用TextWatcher,而不是OnKeyListener。因为TextWatcher能捕获输入法输入和粘贴操作,而OnKeyListener在某些输入法下会失效。
// 搜索框监听示例
mSearchBox.addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// 过滤应用列表
mAllAppsList.setFilter(s.toString());
mAdapter.notifyDataSetChanged();
}
});
其次,预测应用。什么是预测应用?就是根据用户的使用习惯,在搜索框下方推荐几个可能想打开的应用。比如你每天早上9点都会打开“日历”,那到了这个时间点,预测应用就会把“日历”排在前面。
预测应用的实现,核心是UserEventPredictor类。它会记录用户每次打开应用的时间、频率、最近使用时间等数据,然后通过一个评分算法,算出每个应用的“预测分数”。
| 因素 | 权重 | 说明 |
|---|---|---|
| 使用频率 | 0.4 | 过去7天打开的次数 |
| 最近使用时间 | 0.3 | 距离现在越近,分数越高 |
| 时间规律性 | 0.2 | 是否在固定时间段打开 |
| 位置相关性 | 0.1 | 当前是否在常去的地点 |
这个评分算法,我建议不要写死在代码里。因为不同用户的使用习惯差异很大。我曾经在项目中把权重写死了,结果测试时发现,有些用户每天打开“计算器”几十次,但“计算器”的预测排名却不高。后来我改成从云端动态下发权重配置,效果好了很多。
小技巧:预测应用的列表不要超过6个。太多反而让用户选择困难。而且,预测应用应该跟搜索框的输入联动——如果用户已经开始输入,预测应用就变成搜索建议。
5.4 知识体系结构图
下面这张图,帮你理清Hotseat和AllApps的整体架构:
这张图把Hotseat和AllApps的层级关系、核心组件、实现文件都串起来了。你可以把它当作一个快速索引,遇到问题时,先看这张图定位到对应模块,再去翻源码。
好了,这一章的内容就到这里。Hotseat和AllApps是桌面系统里用户接触最多的两个区域,设计得好不好,直接影响用户体验。我个人觉得,与其追求花哨的动画效果,不如先把这两个区域的稳定性和响应速度做好。毕竟,用户打开应用抽屉,是想快速找到应用,而不是看动画。
公众号:蓝海资料掘金营,微信deep3321