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,负责应用列表的展示和交互。
  • AdapterAllAppsListAdapter,绑定应用数据。
  • ModelAllAppsList,维护应用列表的数据模型。

你想想看,当用户打开应用抽屉时,系统需要做两件事:一是加载所有应用,二是按字母排序。这个排序逻辑,我建议放在后台线程做,否则在主线程上处理几百个应用,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 架构图 Launcher Hotseat AllAppsContainerView CellLayout 固定行/列配置 AllAppsRecyclerView 搜索框 + 预测 Hotseat.java hotseat.xml 配置 AllAppsListAdapter UserEventPredictor 固定区域 + 列表展示 + 智能预测 = 高效桌面入口

这张图把Hotseat和AllApps的层级关系、核心组件、实现文件都串起来了。你可以把它当作一个快速索引,遇到问题时,先看这张图定位到对应模块,再去翻源码。

好了,这一章的内容就到这里。Hotseat和AllApps是桌面系统里用户接触最多的两个区域,设计得好不好,直接影响用户体验。我个人觉得,与其追求花哨的动画效果,不如先把这两个区域的稳定性和响应速度做好。毕竟,用户打开应用抽屉,是想快速找到应用,而不是看动画。


公众号:蓝海资料掘金营,微信deep3321