24、Launcher的定制化(ROM开发):默认桌面布局配置、预置应用与Widget、厂商自定义功能

做ROM开发的朋友都知道,Launcher是用户接触手机的第一道门。你想想看,用户开机第一眼看到什么?就是桌面布局。如果默认布局乱七八糟,用户的第一印象就毁了。今天我就聊聊Launcher定制化里最核心的三个点:默认桌面布局、预置应用和Widget、以及厂商自定义功能。

默认桌面布局配置

默认桌面布局,说白了就是用户恢复出厂设置后看到的那个样子。我在项目中遇到过好几次,产品经理说“这个图标放这里,那个文件夹放那里”,结果改起来发现坑不少。

Launcher的默认布局通常由XML配置文件控制。以AOSP原生Launcher3为例,配置文件在:

packages/apps/Launcher3/res/xml/default_workspace_<orientation>.xml

这个文件定义了桌面上的所有元素。我习惯把布局拆成三部分来看:

  • Hotseat(热键栏):底部那排固定图标
  • Workspace(工作区):主屏幕的页面和图标位置
  • Folder(文件夹):预置的文件夹结构

举个例子,一个典型的配置片段长这样:

<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto">
    <!-- Hotseat -->
    <resolve
        launcher:screen="0"
        launcher:x="0"
        launcher:y="0"
        launcher:spanX="1"
        launcher:spanY="1"
        launcher:packageName="com.android.dialer"
        launcher:className="com.android.dialer.DialtactsActivity" />

    <!-- 文件夹 -->
    <folder
        launcher:screen="1"
        launcher:x="2"
        launcher:y="3"
        launcher:title="工具">
        <resolve
            launcher:packageName="com.android.calculator2"
            launcher:className="com.android.calculator2.Calculator" />
        <resolve
            launcher:packageName="com.android.deskclock"
            launcher:className="com.android.deskclock.DeskClock" />
    </folder>
</favorites>
注意:screen、x、y的值不能超出Launcher定义的网格范围。我曾经遇到过配置了screen="5",但Launcher只支持4屏,结果那个图标直接消失了。

这里有个关键点:包名和类名必须写对。大小写错了、包名拼错了,图标都不会显示。我建议你在编译前先用adb查一下目标应用的组件名:

adb shell dumpsys package com.example.app | grep -i "MainActivity"

预置应用与Widget

预置应用,就是ROM里自带的那些App。厂商通常会预装自己的应用商店、浏览器、音乐播放器等。Widget则是桌面上的小部件,比如天气、时钟、搜索框。

预置应用分两种:

  • 系统级预置:放在/system/app或/system/priv-app下,用户无法卸载
  • 用户级预置:放在/data/app下,用户可以卸载

我个人习惯把核心功能做成系统级预置,比如拨号、短信、设置。第三方合作的应用做成用户级预置,方便用户卸载。

Widget的预置稍微复杂一点。Launcher3里,Widget的配置在:

packages/apps/Launcher3/res/xml/default_workspace_widgets.xml

举个例子:

<widgets>
    <widget
        launcher:packageName="com.android.deskclock"
        launcher:className="com.android.deskclock.widget.DeskClockWidget"
        launcher:screen="0"
        launcher:x="0"
        launcher:y="0"
        launcher:spanX="4"
        launcher:spanY="1" />
</widgets>
小技巧:Widget的spanX和spanY要匹配实际尺寸。比如一个4x1的Widget,你写成spanX="2" spanY="2",显示出来就会变形。我建议先在模拟器上拖拽测试,确认尺寸后再写配置。

预置Widget还有一个坑:Widget提供者必须存在。如果预置了一个Widget,但对应的App被卸载了,Launcher启动时会报错。我曾经在项目里遇到过,预置了第三方天气Widget,结果那个App被移除了,导致Launcher崩溃。解决方案是在配置里加一个检查:

// 在Launcher的onCreate中检查Widget是否存在
if (!widgetExists(packageName, className)) {
    // 跳过这个Widget的加载
    Log.w(TAG, "Widget provider not found: " + packageName);
}

厂商自定义功能

厂商自定义功能,说白了就是各家ROM的差异化卖点。比如小米的负一屏、华为的智慧助手、OPPO的侧边栏。这些功能通常通过修改Launcher源码或添加插件实现。

我总结了几种常见的自定义方式:

功能类型 实现方式 难度
负一屏(智能助手) 在Launcher最左侧添加一个Fragment页面 中等
手势操作 修改Launcher的触摸事件分发逻辑
主题换肤 替换Launcher的资源包(图标、壁纸、字体)
应用锁/隐藏应用 在Launcher的App列表中添加过滤逻辑 中等

以负一屏为例,实现思路是这样的:

// 在Launcher的Workspace中添加一个特殊的页面
public class SmartAssistantPage extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // 加载负一屏的布局
        return inflater.inflate(R.layout.smart_assistant, container, false);
    }

    public void updateData() {
        // 更新天气、日程、快递等信息
        // 这里可以调用系统服务或第三方API
    }
}

然后在Launcher的Workspace初始化时,把这个Fragment添加到最左侧:

// 在Launcher.java中
private void initWorkspace() {
    // 添加负一屏
    SmartAssistantPage assistantPage = new SmartAssistantPage();
    getSupportFragmentManager().beginTransaction()
        .add(R.id.workspace, assistantPage, "assistant")
        .commit();

    // 原有的桌面页面
    for (int i = 0; i < DEFAULT_PAGE_COUNT; i++) {
        addDefaultPage(i);
    }
}

核心思路:厂商自定义功能的核心是“不破坏Launcher原有架构”。尽量通过扩展点(如Fragment、BroadcastReceiver、ContentProvider)来实现,而不是直接修改Launcher的核心逻辑。这样后续升级AOSP版本时,冲突会少很多。

嗯,这里还要提一下手势操作。我记得有一次,产品要求实现“双指下滑打开通知栏”。这个功能看似简单,但实现起来要改Launcher的触摸事件分发。我当时的做法是:

// 在Launcher的onInterceptTouchEvent中
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getPointerCount() == 2 && ev.getAction() == MotionEvent.ACTION_MOVE) {
        // 检测双指下滑手势
        float deltaY = ev.getY(0) - lastY;
        if (deltaY > THRESHOLD) {
            // 展开通知栏
            expandNotificationPanel();
            return true;
        }
    }
    return super.onInterceptTouchEvent(ev);
}

这个方案有个问题:双指手势和单指手势容易冲突。用户可能只是想双指缩放桌面,结果触发了通知栏。后来我加了一个时间阈值,只有双指下滑超过200ms才触发,才解决了这个问题。

知识体系总览

下面这张图总结了Launcher定制化的核心逻辑:

Launcher定制化知识体系 Launcher定制化核心目标 默认桌面布局配置 预置应用与Widget 厂商自定义功能 子项 • Hotseat图标位置 • Workspace页面数 • 文件夹结构 • 网格行列数 • 壁纸与主题 子项 • 系统级预置App • 用户级预置App • Widget尺寸配置 • Widget提供者检查 • 预置应用权限 子项 • 负一屏实现 • 手势操作 • 主题换肤 • 应用锁/隐藏 • 侧边栏/悬浮球

这张图把Launcher定制化分成了三个模块。每个模块都有对应的配置文件和实现方式。做ROM开发时,我建议你从默认布局开始,先把基础架子搭好,再逐步添加预置应用和自定义功能。这样出问题的概率会小很多。

最后说一句:Launcher定制化没有标准答案。不同厂商、不同机型、不同Android版本,实现方式都可能不一样。关键是要理解Launcher的架构,知道哪些地方可以改、哪些地方不能动。嗯,这个需要在实际项目中慢慢积累经验。


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