前言
最近因为业务需要,需要做一款 UI 定制的鸿蒙 Launcher,于是就开始了「找到代码」、「研究代码」、「魔改代码」的套路流程,仅以此文章作为知识备份和技术探讨所用,也希望能给其他小伙伴提供一些源码的解析思路,方法大家各自魔改!
一、官方简介
Gitee codes:应用子系统/Launcher
Launcher 作为系统人机交互的首要入口,提供应用图标的显示、点击启动、卸载应用,并提供桌面布局设置以及最近任务管理等功能。
Launcher 采用扩展的 TS 语言(ArkTS)开发
1.1 主要结构
1.2 分层说明
Module | 层级 | 说明 |
---|---|---|
product | 业务形态层 | 区分不同产品、不同屏幕的各形态桌面,含有桌面窗口、个性化业务,组件的配置,以及个性化资源包。 |
feature | 公共特性层 | 抽象的公共特性组件集合,可以被各桌面形态引用。 |
common | 公共能力层 | 基础能力集,每个桌面形态都必须依赖的模块。 |
1.3 目录结构
/applications/standard/launcher/
├── common # 公共能力层目录
├── docs # 开发指南
├── feature # 公共特性层目录
│ └── appcenter # 应用中心
│ └── bigfolder # 智能文件夹
│ ├── form # 桌面卡片管理功能
│ ├── gesturenavigation # 手势导航
│ ├── pagedesktop # 工作区
│ ├── recents # 最近任务
│ ├── settings # 桌面设置
│ ├── smartdock # dock工具栏
├── product # 业务形态层目录
├── signature # 签名证书
1.4 开发调试
IDE 下载:建议大家直接下载 OpenHarmony 4.1 Release DevEco-Studio 吧,API 支持 8 ~ 11。
1.5 SDK
Launcher 应用的编译需使用相对应版本的 ohos-sdk-full \ mac-sdk-full 来进行开发调试。
IDE 上是 Public SDK,故 full sdk 需要重新下载,下载地址:
新版本界面:http://ci.openharmony.cn/workbench/cicd/dailybuild/dailylist
老版本界面:http://ci.openharmony.cn/dailys/dailybuilds
具体下载及如何替换这边就不啰嗦了,大家直接看 Gitee 介绍自行替换。
1.6 签名配置
关于签名配置,也不啰嗦了,下载的代码自带的文件都已经配置好,无需自己手动签名。
1.7 替换 Launcher
使用以下命令来更新编译出来的 Launcher 部件 hap 包:
ren phone_launcher-default-signed.hap Launcher.hap
ren launcher_settings-phone_launcher-default-signed.hap Launcher_Settings.hap
hdc target mount
hdc shell rm -rf /data/misc_de/0/mdds/0/default/bundle_manager_service
hdc shell rm -rf /data/accounts
hdc shell mount -o remount,rw /
hdc file send .\Launcher.hap /system/app/com.ohos.launcher/Launcher.hap
hdc file send .\Launcher_Settings.hap /system/app/com.ohos.launcher/Launcher_Settings.hap
pause
hdc shell mount -o remount,rw /
hdc shell rm /data/ -rf
hdc shell sync /system/bin/udevadm trigger
hdc shell reboot
二、编译运行
2.1 分支选择
拉完官方示例代码后,可以看到很多分支,我选了 OpenHarmony-4.1-Release 作为魔改的基础分支,当然你也可以根据需要选择别的分支(我是着实看不懂,搞这么多分支干什么,而且基本上彼此分支的 UI 效果大差不差,几乎所有 Openharmony 自带的系统应用 Demo UI 及功能逻辑都很 low,所以凡事靠自己,自己魔改吧!)
2.2 打开工程 / 编译 hap
切到对应分支后,即可打开工程,等待同步完成,如下图即可。
接下来可以编译 hap 包:
接着找到需要的 hap 包,重命名,替换后重启:
默认 Launcher 效果:(我手里有一台平板,所以就以平板为示例,效果要比手机少一点)
三、Launcher 首页
3.1 MainAbility
export default class MainAbility extends ServiceExtension {
onCreate(want: Want): void {
Log.showInfo(TAG,'onCreate start');
this.context.area = 0;
this.initLauncher();
}
async initLauncher(): Promise<void> {
/**
* 1. init Launcher context
* 初始化上下文
*/
globalThis.desktopContext = this.context;
/**
* 2. init global const
* 初始化全局变量
*/
this.initGlobalConst();
/**
* 3. init Gesture navigation
* 初始化手势导航
*/
this.startGestureNavigation();
/**
* 4. init rdb
* 初始化 rdb
*/
let dbStore = RdbStoreManager.getInstance();
await dbStore.initRdbConfig();
await dbStore.createTable();
let registerWinEvent = (win: window.Window) => {
win.on('windowEvent', (stageEventType) => {
// 桌面获焦或失焦时,通知桌面的卡片变为可见状态
if (stageEventType === window.WindowEventType.WINDOW_ACTIVE) {
localEventManager.sendLocalEventSticky(EventConstants.EVENT_REQUEST_FORM_ITEM_VISIBLE, null);
Log.showInfo(TAG, `lifeCycleEvent change: ${stageEventType}`);
}
})
};
/**
* 5. 注册窗口事件
*/
windowManager.registerWindowEvent();
/**
* 6. 注册导航栏事件
*/
navigationBarCommonEventManager.registerNavigationBarEvent();
/**
* 7. create Launcher entry view
* 创建桌面窗口
* WindowManager.ts --> DESKTOP_WINDOW_NAME = 'EntryView';
* 加载 pages/EntryView
*/
windowManager.createWindow(globalThis.desktopContext, windowManager.DESKTOP_WINDOW_NAME,
windowManager.DESKTOP_RANK, 'pages/' + windowManager.DESKTOP_WINDOW_NAME, true, registerWinEvent);
/**
* 8. load recent,加载 Recent 窗口
*/
windowManager.createRecentWindow();
this.registerInputConsumer();
}
...
}
MainAbility 创建了桌面窗口:pages/EntryView。
3.2 EntryView
📄 EntryView.ets
@Entry
@Component
struct EntryView {
build() {
Stack() {
Flex({ direction: FlexDirection.Column, ... }) {
Column() {
// 1. 桌面布局,类似于 Android Launcher 的 CellLayout
PageDesktopLayout();
}
.height(this.workSpaceHeight)
.onAreaChange((oldValue: Area, newValue: Area) => {
Log.showDebug(TAG, `onAreaChange navigationBarStatus: ${this.navigationBarStatus}`);
if (JSON.stringify(oldValue) == JSON.stringify(newValue)) return;
if (this.navigationBarStatus == "1") {
setTimeout(() => {
SettingsModel.getInstance().setValue(this.navigationBarStatus);
}, 50)
}
})
Column() {
// 2. Dock 区域,类似于 Android 的 Hotseat
SmartDock();
}
.height(this.dockHeight)
}
FolderOpenComponent();
}
.backgroundImage(StyleConstants.DEFAULT_BACKGROUND_IMAGE)
.backgroundImageSize(ImageSize.Cover)
.backgroundImagePosition(Alignment.Center)
.width('100%')
.height('100%')
}
}
3.3 PageDesktopLayout()
所以,我们再来看看 PageDesktopLayout() 的源码:
@Component
export struct PageDesktopLayout {
build() {
// 自定义的 GridSwiper 组件
GridSwiper({
gridConfig: this.gridConfig,
mPageDesktopViewModel: mPageDesktopViewModel,
dialogController: this.deviceType == CommonConstants.PAD_DEVICE_TYPE ? null : this.dialogController
}).id(`${TAG}`)
.width(StyleConstants.PERCENTAGE_100)
.height(StyleConstants.PERCENTAGE_100)
}
}
3.4 GridSwiper
继续跟踪源码:
@Component
export default struct GridSwiper {
build() {
Column() {
if (this.buildLog()) {}
if (this.desktopLoadFinished) {
// 1. 轮播布局
Swiper(this.swiperController) {
ForEach(this.pageList, (item: number, index: number) => {
// 判断设备类型
if (AppStorage.get('deviceType') == CommonConstants.DEFAULT_DEVICE_TYPE) {
Column() {
SwiperPage({
appListInfo: $appListInfo,
swiperPage: index.valueOf(),
gridConfig: this.gridConfig,
mPageDesktopViewModel: this.mPageDesktopViewModel
}).id(`SwiperPage_${item}${index}`)
}
.gesture(
LongPressGesture({ repeat: false })
.onAction((event: GestureEvent) => {
this.dialogController?.open();
})
)
.bindContextMenu(this.MenuBuilder, ResponseType.RightClick)
} else {
SwiperPage({
appListInfo: $appListInfo,
swiperPage: index.valueOf(),
gridConfig: this.gridConfig,
mPageDesktopViewModel: this.mPageDesktopViewModel
}).id(`SwiperPage_${item}${index}`)
.bindContextMenu(this.MenuBuilder, ResponseType.LongPress)
.bindContextMenu(this.MenuBuilder, ResponseType.RightClick)
}
}, (item: number, index: number) => {
return `${item}${index}`;
})
}
.id(`${TAG}_Swiper`)
...
}
}
.id(`${TAG}`)
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.height(StyleConstants.PERCENTAGE_100)
.width(StyleConstants.PERCENTAGE_100)
}
}
我们忽略掉一些多余的代码,只看核心部分,发现都会调用 SwiperPage 组件,我们继续跟:
3.5 SwiperPage
@Component
export default struct SwiperPage {
build() {
// 1. 网格布局
Grid() {
ForEach(this.mAppListInfo, (item: LauncherDragItemInfo, index: number) => {
// 2. 自组件
GridItem() {
if (this.buildLog(item)) {
}
// 3. 如果类型是 APP
if (item.typeId === CommonConstants.TYPE_APP) {
// 4. 具体每一个应用
AppItem({
item: item,
mPageDesktopViewModel: this.mPageDesktopViewModel,
mNameLines: this.mNameLines
}).id(`${TAG}_AppItem_${index}`)
} else if (item.typeId === CommonConstants.TYPE_FOLDER) {
FolderItem({
folderItem: item,
mPageDesktopViewModel: this.mPageDesktopViewModel,
mNameLines: this.mNameLines
}).id(`${TAG}_FolderItem_${index}`)
} else if (item.typeId === CommonConstants.TYPE_CARD) {
FormItem({
formItem: item
}).id(`${TAG}_FormItem_${index}`)
}
}
.id(`${TAG}_GridItem_${index}`)
...
}, (item: LauncherDragItemInfo, index: number) => {
if (item.typeId === CommonConstants.TYPE_FOLDER) {
return JSON.stringify(item);
} else if (item.typeId === CommonConstants.TYPE_CARD) {
return JSON.stringify(item) + this.formRefresh;
} else if (item.typeId === CommonConstants.TYPE_APP) {
return JSON.stringify(item);
} else {
return '';
}
})
}
.id(`${TAG}_Grid_${this.swiperPage}`)
...
}
}
3.6 AppItem
@Component
export default struct AppItem {
build() {
Column() {
// 又是一个 AppBubble
AppBubble({
iconSize: this.mIconSize,
nameSize: this.mAppNameSize,
nameHeight: this.mAppNameHeight,
nameFontColor: this.mPageDesktopViewModel?.getPageDesktopStyleConfig().mNameFontColor as string,
appName: this.item.appName,
bundleName: this.item.bundleName,
abilityName: this.item.abilityName,
moduleName: this.item.moduleName,
appIconId: this.item.appIconId,
appLabelId: this.item.appLabelId,
badgeNumber: this.item.badgeNumber,
isSelect: this.selectDesktopAppItem == this.item.keyName,
getMenuInfoList: this.getMenuInfoList,
mPaddingTop: this.mMarginVertical,
nameLines: this.mNameLines,
mIconNameMargin: this.mIconNameMargin,
dragStart: this.dragStart
})
}
.visibility(...)
.onMouse((event: MouseEvent) => {
...
})
.onClick((event) => {
...
})
.onTouch((event: TouchEvent) => {
...
})
.width(this.mAppItemWidth)
.height(this.mAppItemWidth)
}
}
3.7 AppBubble
@Component
export struct AppBubble {
build() {
Column() {
Column() {
Column() {
// 应用图标
AppIcon({
iconSize: this.iconSize,
iconId: this.appIconId,
bundleName: this.bundleName,
moduleName: this.moduleName,
icon: ResourceManager.getInstance().getCachedAppIcon(this.appIconId, this.bundleName, this.moduleName),
badgeNumber: this.badgeNumber,
useCache: this.useCache
})
}
.onDragStart((event: DragEvent, extraParams: string) => {
return this.dragStart(event);
})
.bindContextMenu(this.MenuBuilder, ResponseType.LongPress)
.onDragEnd((event: DragEvent, extraParams: string) => {
...
})
// 应用名称
AppName({
nameHeight: this.nameHeight,
nameSize: this.nameSize,
nameFontColor: this.nameFontColor,
bundleName: this.bundleName,
moduleName: this.moduleName,
appName: this.appName,
labelId: this.appLabelId,
useCache: this.useCache,
nameLines: this.nameLines,
marginTop: this.mIconNameMargin
})
}
.bindContextMenu(this.MenuBuilder, ResponseType.RightClick)
...
}
.parallelGesture(
...
)
}
}
看到这,是不是整个桌面的图标区域结构豁然开朗?看个图: