【 OpenHarmony 系统应用源码解析 】-- Launcher 初体验

前言

最近因为业务需要,需要做一款 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(
      ...
    )
  }

}

看到这,是不是整个桌面的图标区域结构豁然开朗?看个图:

在这里插入图片描述

  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值