HarmonyOS6 - 鸿蒙读取系统联系人实战案例

HarmonyOS6 - 鸿蒙读取系统联系人实战案例

开发环境为:

开发工具:DevEco Studio 6.0.1 Release

API版本是:API21

本文所有代码都已使用模拟器测试成功!

1. 需求

首先我们设定如下需求:

  1. 点击添加,就弹框显示手机联系人列表
  2. 勾选联系人,勾选完毕后,点击确认,勾选的联系人数据,就显示到APP中这个页面中
  3. 这个页面中,可以多选联系人,点击删除,就删除了联系人数据
  4. 联系人数据存储在APP本地数据库中
  5. 应用重启后联系人数据依然需要存在

2. 分析

针对上面需求,我的分析思路如下:

  1. 页面结构设计

设计联系人列表页面的整体布局结构,包括顶部导航栏、联系人列表区域和底部操作栏。

  1. 数据模型定义

定义联系人数据接口(FriendsItem)和电话信息接口(PhoneItem),用于规范数据类型。

  1. 数据库操作实现
  • 封装RDB数据库工具类(RdbTools)
  • 实现数据增删改查功能
  • 添加防重复逻辑,通过手机号判断联系人是否已存在
  1. 核心功能开发

(1)联系人展示

  • 页面加载时从数据库查询并显示所有联系人
  • 使用List组件渲染联系人列表
  • 添加数据加载完成提示

(2)联系人添加

  • 集成系统联系人选择组件
  • 批量添加联系人功能
  • 实时反馈添加结果(新增成功/重复提醒)

(3)多选删除功能

  • 长按联系人进入多选模式
  • 显示勾选框供用户选择
  • 实现全选/取消全选功能
  • 批量删除选中的联系人
  1. 用户体验优化
  • 添加多选模式下的视觉反馈(选中背景色变化)
  • 操作结果提示(弹窗消息)
  • 底部按钮动态切换(添加/取消、选择/删除)
  • 手势交互(长按触发多选)
  1. 异步处理优化
  • 解决异步操作顺序问题,确保日志正确显示
  • 使用async/await确保数据处理的顺序性
  • 优化Promise链式调用,避免状态混乱
  1. 状态管理
  • 使用@State管理组件状态
  • 维护多选模式状态
  • 管理选中联系人ID集合
  • 确保UI状态与数据同步

3. 编码

根据以上思路,我们开始编写代码

首先新建页面【ContactsDetail.ets】,代码如下:

js 复制代码
import { router } from "@kit.ArkUI";
import { contact } from "@kit.ContactsKit";
import { myTools } from "../common/MyTools";
import { ObjectModel } from "../common/rdb/ObjectModel";
import RdbTools from "../common/rdb/RdbTools";

export interface FriendsItem {
  id?: number;
  name?: string;
  phone?: string;
}

export interface PhoneItem {
  labelId?: number;
  labelName?: string;
  phoneNumber?: string;
}

const TAG = 'ContactsDetail'

/**
 * 联系人列表(可添加系统联系人)
 */
interface PhoneItemInterface {
  isNew: boolean;
  phone: string;
}

@Entry
@Component
struct ContactsDetail {
  // 联系人数据集合
  @State friendsList: FriendsItem[] = [];
  // 选中的联系人ID集合
  @State selectedIds: Set<number> = new Set<number>();
  // 是否处于多选模式
  @State isMultiSelectMode: boolean = false;

  //新增联系人信息到RDB数据库
  async addDataToRDB(friend: FriendsItem): Promise<PhoneItemInterface> {
    return new Promise(async (resolve) => {
      //新增之前,先查询数据库中是否已经存在该手机号
      await RdbTools.queryData(friend.phone).then(async res => {
        console.info(TAG, '查询手机号[' + friend.phone + ']结果=' + JSON.stringify(res))
        if (res.length > 0) {
          console.info(TAG, '查询手机号[' + friend.phone + ']数据库已存在,无需新增!')
          resolve({ isNew: false, phone: friend.phone as string }); // 重复添加
        } else {
          console.info(TAG, '查询手机号[' + friend.phone + ']数据库不存在,需要新增!')
          //数据保存到RDB数据库
          await RdbTools.insertData({
            id: friend.id,
            name: friend.name,
            phone: friend.phone,
          } as ObjectModel)
          //加到列表集合中
          this.friendsList.push(friend);
          resolve({ isNew: true, phone: friend.phone as string }); // 新添加
        }
      })
    });
  }

  //查询RDB数据库中的所有联系人数据
  async queryAllDB() {
    await RdbTools.queryData('').then(res => {
      console.info(TAG, '查询数据库所有联系人信息=' + JSON.stringify(res))
      if (res) {
        this.friendsList = res
        console.info(TAG, 'this.friendsList=' + JSON.stringify(this.friendsList))
        myTools.alertMsg('数据加载完毕!')
      }
    })
  }

  aboutToAppear(): void {
    //需要等待RDB数据库初始化完毕后才能查询数据库,所以这里进入页面后,1秒之后才查询数据库
    setTimeout(() => {
      this.queryAllDB();
    }, 1000)
  }

  // 选择联系人
  async selectContacts() {
    // 选择联系人时的筛选条件 (是否多选)
    let contactSelectionOptions: contact.ContactSelectionOptions = { isMultiSelect: true };
    // 调用通讯录选择组件,让用户选择需要传入APP的通讯录联系人
    let promise = contact.selectContacts(contactSelectionOptions);

    await promise.then(async (data) => {
      // 用户选择确认之后,会在此处收到回调
      console.info(TAG, `selectContacts success: data->${JSON.stringify(data)}`);
      if (data && data.length > 0) {
        // 用于收集结果的数组
        const chongfuPhone: string[] = [];
        const newPhone: string[] = [];

        // 使用for循环按顺序处理每个联系人,并等待每个处理完成
        for (const contactItem of data) {
          let phoneItemArray: Array<PhoneItem> = JSON.parse(JSON.stringify(contactItem.phoneNumbers));
          let friend: FriendsItem = {
            id: contactItem.id,
            name: contactItem.name?.fullName,
            phone: phoneItemArray[0].phoneNumber
          };

          // 等待每个联系人处理完成
          const result = await this.addDataToRDB(friend);

          // 根据结果分类
          if (result.isNew) {
            newPhone.push(result.phone);
          } else {
            chongfuPhone.push(result.phone);
          }
        }

        console.info(TAG, `处理完成后的this.friendsList=${JSON.stringify(this.friendsList)}`);
        console.info(TAG, `处理完成后的newPhone.length=${newPhone.length}`);
        console.info(TAG, `处理完成后的chongfuPhone.length=${chongfuPhone.length}`);

        //弹框提示新增结果
        if (newPhone.length > 0) {
          if (chongfuPhone.length > 0) {
            myTools.alertMsg('新增' + newPhone.length + '个联系人成功!重复手机号有:' +
            chongfuPhone.toString(), 5000);
          } else {
            myTools.alertMsg('新增' + newPhone.length + '个联系人成功!', 5000);
          }
        } else {
          if (chongfuPhone.length > 0) {
            myTools.alertMsg('新增0个联系人成功!重复手机号有:' +
            chongfuPhone.toString(), 5000);
          }
        }
      }
    }).catch((err: BusinessError) => {
      console.error(TAG, `selectContacts fail: err->${JSON.stringify(err)}`);
    });
  }

  //删除RDB数据库中的联系人
  async deletePhone(item: FriendsItem) {
    await RdbTools.deleteData(item.id).then(() => {
      this.queryAllDB();
    });
  }

  // 批量删除选中的联系人
  async deleteSelectedContacts() {
    if (this.selectedIds.size === 0) {
      myTools.alertMsg('请先选择要删除的联系人!');
      return;
    }

    myTools.alertDialogRollback('请确认:', `确定删除选中的${this.selectedIds.size}个联系人吗?`, async () => {
      try {
        // 批量删除选中的联系人
        const deletePromises = Array.from(this.selectedIds).map(async id => {
          await RdbTools.deleteData(id).then(() => {
          });
        }
        );
        await Promise.all(deletePromises);
        // 删除成功后更新列表
        await this.queryAllDB();
        // 清空选中集合
        this.selectedIds.clear();
        // 退出多选模式
        this.isMultiSelectMode = false;

        myTools.alertMsg('删除成功!');
      } catch (error) {
        console.error(TAG, '批量删除失败:', error);
        myTools.alertMsg('删除失败,请重试!');
      }
    }, () => {
      // 取消操作
    });
  }

  // 切换多选模式
  toggleMultiSelectMode() {
    this.isMultiSelectMode = !this.isMultiSelectMode;
    if (!this.isMultiSelectMode) {
      // 退出多选模式时清空选中状态
      this.selectedIds.clear();
    }
  }

  // 取消多选模式
  cancelMultiSelectMode() {
    this.isMultiSelectMode = false;
    this.selectedIds.clear();
  }

  // 选中/取消选中联系人
  toggleSelectContact(item: FriendsItem) {
    if (!item.id) {
      return;
    }

    if (this.selectedIds.has(item.id)) {
      this.selectedIds.delete(item.id);
    } else {
      this.selectedIds.add(item.id);
    }

    // 如果所有项都取消选中,退出多选模式
    if (this.selectedIds.size === 0) {
      this.isMultiSelectMode = false;
    }
  }

  // 全选/取消全选
  toggleSelectAll() {
    if (this.selectedIds.size === this.friendsList.length) {
      // 如果已经全选,则取消全选
      this.selectedIds.clear();
    } else {
      // 全选
      this.selectedIds = new Set(this.friendsList.filter(item => item.id).map(item => item.id!));
    }
  }

  // 勾选框组件
  @Builder
  checkboxBuilder(item: FriendsItem) {
    Checkbox()
      .select(this.selectedIds.has(item.id!))
      .selectedColor('#007DFF')
      .width(24)
      .height(24)
      .margin({ right: 12 })
      .onChange((checked: boolean) => {
        if (checked) {
          this.selectedIds.add(item.id!);
        } else {
          this.selectedIds.delete(item.id!);
        }
      })
  }

  build() {
    Column() {
      // 顶部导航栏
      Row() {
        Image($r('app.media.back'))
          .width(24)
          .height(24)
          .margin({ left: 16 })
          .onClick(() => {
            if (this.isMultiSelectMode) {
              // 如果处于多选模式,点击返回退出多选模式
              this.cancelMultiSelectMode();
            } else {
              router.back();
            }
          })

        Text(this.isMultiSelectMode ? `已选中${this.selectedIds.size}项` : '常用联系人')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .fontColor('#000000')
          .layoutWeight(1)
          .textAlign(TextAlign.Center)

        // 多选模式下的操作按钮
        if (this.isMultiSelectMode) {
          Text(this.selectedIds.size === this.friendsList.length ? '取消全选' : '全选')
            .fontSize(16)
            .fontColor('#007DFF')
            .margin({ right: 16 })
            .onClick(() => {
              this.toggleSelectAll();
            })
        } else {
          // 非多选模式下的占位
          Row()
            .width('50%')
            .height(24)
            .margin({ right: 16 })
        }
      }
      .width('100%')
      .height(56)
      .backgroundColor('#FFFFFF')
      .alignItems(VerticalAlign.Center)
      .justifyContent(FlexAlign.Start)

      // 内容区域
      List() {
        ForEach(this.friendsList, (item: FriendsItem, index: number) => {
          ListItem() {
            Row() {
              // 多选模式下显示勾选框
              if (this.isMultiSelectMode) {
                this.checkboxBuilder(item)
              }

              Column() {
                Text(item.name)
                  .fontSize(16)
                  .fontColor('#333333')
                  .margin({ bottom: 4 })
                Text(item.phone)
                  .fontSize(14)
                  .fontColor('#999999')
              }
              .alignItems(HorizontalAlign.Start)

              Blank()

              // 非多选模式下显示箭头图标
              if (!this.isMultiSelectMode) {
                Image($r('app.media.next'))
                  .width(20)
                  .height(20)
                  .objectFit(ImageFit.Cover)
              }
            }
            .width('100%')
            .padding({
              top: 16,
              bottom: 16,
              left: 20,
              right: 20
            })
            .backgroundColor(this.isMultiSelectMode && this.selectedIds.has(item.id!) ? '#F0F8FF' : '#FFFFFF')
            .onClick(() => {
              if (this.isMultiSelectMode) {
                // 多选模式下点击切换选中状态
                this.toggleSelectContact(item);
              }
            })
            .gesture(
              LongPressGesture({ duration: 500 })
                .onAction(() => {
                  // 长按进入多选模式并选中当前项
                  if (!this.isMultiSelectMode) {
                    this.isMultiSelectMode = true;
                  }
                  if (item.id) {
                    this.toggleSelectContact(item);
                  }
                })
            )
          }
        })
      }
      .width('95%')
      .borderRadius(8)
      .margin({ top: 12 })
      .backgroundColor('#FFFFFF')
      .divider({
        strokeWidth: 1,
        startMargin: 20,
        endMargin: 20,
        color: '#ffe9f0f0'
      })
      .layoutWeight(1)

      // 底部Tab栏
      Row() {
        // 左侧按钮:多选模式下显示"取消",非多选模式下显示"添加"
        Column() {
          if (this.isMultiSelectMode) {
            // 多选模式下的取消按钮
            Text('×')  // 取消符号
              .fontSize(24)
              .fontColor('#333333')
            Text('取消')
              .fontSize(12)
              .fontColor('#333333')
              .margin({ top: 4 })
          } else {
            // 非多选模式下的添加按钮
            Text('+')
              .fontSize(24)
              .fontColor('#333333')
            Text('添加')
              .fontSize(12)
              .fontColor('#333333')
              .margin({ top: 4 })
          }
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .height('100%')
        .onClick(() => {
          if (this.isMultiSelectMode) {
            // 多选模式下点击取消按钮,退出多选模式
            this.cancelMultiSelectMode();
          } else {
            // 非多选模式下点击添加按钮,添加联系人
            let isContactsAvailable = canIUse('SystemCapability.Applications.Contacts');
            if (isContactsAvailable) {
              this.selectContacts();
            } else {
              console.info(TAG, 'The device does not support the ability to select contacts.');
            }
          }
        })

        // 右侧按钮:多选模式下显示"删除",非多选模式下显示"选择"
        Column() {
          if (this.isMultiSelectMode) {
            // 多选模式下的删除按钮
            Text('\u{1F5D1}')  // Unicode 垃圾箱符号
              .fontSize(22)
              .fontColor(this.selectedIds.size > 0 ? '#FF3B30' : '#999999')
            Text(`删除(${this.selectedIds.size})`)
              .fontSize(12)
              .fontColor(this.selectedIds.size > 0 ? '#FF3B30' : '#999999')
              .margin({ top: 4 })
          } else {
            // 非多选模式下的选择按钮
            Text('\u{2705}')  // Unicode 对勾符号
              .fontSize(22)
              .fontColor('#333333')
            Text('选择')
              .fontSize(12)
              .fontColor('#333333')
              .margin({ top: 4 })
          }
        }
        .layoutWeight(1)
        .alignItems(HorizontalAlign.Center)
        .justifyContent(FlexAlign.Center)
        .height('100%')
        .onClick(() => {
          if (this.isMultiSelectMode) {
            // 多选模式下执行批量删除
            if (this.selectedIds.size > 0) {
              this.deleteSelectedContacts();
            } else {
              myTools.alertMsg('请先选择要删除的联系人!');
            }
          } else {
            // 非多选模式下进入多选模式
            this.toggleMultiSelectMode();
          }
        })
      }
      .width('100%')
      .height(70)
      .backgroundColor('#F5F5F5')
      .padding({ top: 8, bottom: 8 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }
}

RdbTools工具类代码如下:

js 复制代码
import { relationalStore } from "@kit.ArkData"
import Logger from "../Logger";
import { myTools } from "../MyTools";
import { ObjectModel } from "./ObjectModel";

/**
 * 操作本地RDB数据库的工具类
 */
export default class RdbTools {

  private static rdbStore: relationalStore.RdbStore;

  static setStore(store: relationalStore.RdbStore) {
    RdbTools.rdbStore = store;
  }

  static getStore(): relationalStore.RdbStore {
    return RdbTools.rdbStore;
  }

  // 表名
  static tableName: string = 'phone_book'
  //数据库文件名
  static rdbName: string = 'phoneBook.db'
  // SQL 语法:(SQL语法的数据类型关键词不一样,可通过AI生成SQL语句)
  // 解释说明:
  //   CREATE TABLE IF NOT EXISTS     如果表不存在才创建新的表
  //   INTEGER -> number              INTEGER 整数型   FLOAT 浮点数
  //   PRIMARY KEY                    主键(唯一标识)
  //   AUTOINCREMENT                  自增
  //   TEXT -> string                 字符串型
  //   NOT NULL                       不允许空
  static sqlCreate: string = `CREATE TABLE IF NOT EXISTS ${RdbTools.tableName} (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        phone TEXT NOT NULL
      )`

  // 创建数据库文件
  static async initDataBase() {
    try {
      // 获取操作数据库的管理对象(如果数据库文件不存在,会自动创建数据库文件)
      const store = RdbTools.rdbStore;
      // 执行创建表的语句 execute 执行
      store.executeSql(RdbTools.sqlCreate)
      Logger.debug('数据库文件创建成功!')
    } catch (error) {
      Logger.error('创建数据库文件,发生异常=' + JSON.stringify(error))
    }
  }

  // 新增单条数据
  static async insertData(item: ObjectModel) {
    Logger.debug('新增数据=' + JSON.stringify(item))
    try {
      // 获取操作数据库的对象
      const store = RdbTools.rdbStore;
      Logger.debug('store=' + JSON.stringify(store))
      // 添加一条数据
      const id = await store.insert(RdbTools.tableName, item)
      Logger.debug('新增单条数据成功,id=' + id);
    } catch (error) {
      myTools.alertMsg('新增数据,发生异常:' + JSON.stringify(error))
      Logger.error('新增单条数据,发生异常=' + JSON.stringify(error))
    }
  }

  // 新增多条数据
  static async insertArrayData(item: Array<ObjectModel>) {
    try {
      // 获取操作数据库的对象
      const store = RdbTools.rdbStore;
      // 添加一条数据
      const id = await store.batchInsert(RdbTools.tableName, item)
      Logger.debug('新增多条数据成功,id=' + id)
    } catch (error) {
      myTools.alertMsg('批量新增数据,发生异常:' + JSON.stringify(error))
      Logger.error('新增多条数据,发生异常=' + JSON.stringify(error))
    }
  }

  //删除数据
  static async deleteData(id?: number, ids?: Array<number>) {
    Logger.debug('需要删除的id=' + id)
    Logger.debug('需要删除的ids=' + ids)
    try {
      // 获取操作数据库对象
      const store = RdbTools.rdbStore;
      const predicates = new relationalStore.RdbPredicates(RdbTools.tableName)
      // 注意!!!:记得添加 predicates 限定条件,否者会删除所有数据
      // predicates.equalTo('id', 2)        // equalTo 删除一条
      if (id) {
        predicates.equalTo('id', id) // equalTo 删除一条
      } else {
        predicates.in('id', ids) //  in      批量删除
      }
      const affectedRows = await store.delete(predicates)
      Logger.debug('删除数据,受影响行数=' + affectedRows)
      if (affectedRows == 0) {
        myTools.alertMsg('数据删除失败!')
      } else {
        myTools.alertMsg('数据删除成功!')
      }
    } catch (error) {
      myTools.alertMsg('删除数据,发生异常:' + JSON.stringify(error))
      Logger.error('删除数据,发生异常=' + JSON.stringify(error))
    }
  }

  //更新数据
  static async updateData(item: ObjectModel) {
    if (!item.id) {
      AlertDialog.show({
        message: 'ID缺失,无法修改!'
      })
      return;
    }
    try {
      // 获取操作数据库对象
      const store = RdbTools.rdbStore;
      // 谓词(条件)
      const predicates = new relationalStore.RdbPredicates(RdbTools.tableName)
      // 注意!!!:记得添加 predicates 限定条件,否者修改全部
      predicates.equalTo('id', item.id)
      const affectedRows = await store.update(item, predicates)
      Logger.debug('修改数据 - 受影响行数:' + affectedRows)
    } catch (error) {
      myTools.alertMsg('更新数据,发生异常:' + JSON.stringify(error))
      Logger.debug('更新数据,发生异常=' + JSON.stringify(error))
    }
  }

  // 查询数据
  static async queryData(phone?: string) {
    // 准备一个数组,用于存储数据库提取的数据
    const list: ObjectModel [] = []
    try {
      Logger.debug('>>>>>>>>>>>>> 1')
      // 获取操作数据库的对象
      const store = RdbTools.rdbStore;
      Logger.debug('>>>>>>>>>>>>> 2')
      // 谓词(条件)
      const predicates = new relationalStore.RdbPredicates(RdbTools.tableName)
      Logger.debug('>>>>>>>>>>>>> 3')
      predicates.orderByDesc('id') // 倒序(由大到小,常用于排序)
      Logger.debug('>>>>>>>>>>>>> 4')
      if (phone) {
        Logger.debug('>>>>>>>>>>>>> 5')
        predicates.like('phone', '%' + phone + '%') //  模糊匹配查询
      }
      // predicates.orderByAsc('id')   // 正序(小到大,常用于排序)
      // predicates.equalTo('id', 1)    // 等于(常用于详情页)
      // predicates.in('id', [1, 3, 5]) // 查找多项(常用批量删除)
      // predicates.like('title', '%哈%') //  模糊匹配(常用于搜索)
      // resultSet 结果集
      Logger.debug('>>>>>>>>>>>>> 6')
      const resultSet = await store.query(predicates)
      Logger.debug('>>>>>>>>>>>>> 7')
      // resultSet.goToNextRow()   指针移动到下一行
      Logger.debug('查询RDB数据,结果集数量条数=' + resultSet.rowCount)
      while (resultSet.goToNextRow()) {
        // 移动指针的时候提取数据,按列下标提取数据
        list.push({
          // resultSet.getColumnIndex()        根据列名称获取下标(索引)
          id: resultSet.getLong(resultSet.getColumnIndex('id')),
          name: resultSet.getString(resultSet.getColumnIndex('name')),
          phone: resultSet.getString(resultSet.getColumnIndex('phone')),
        })
      }
      Logger.debug('查询RDB数据,结果集=' + JSON.stringify(list))
    } catch (error) {
      myTools.alertMsg('查询数据,发生异常:' + JSON.stringify(error))
      Logger.error('查询数据,发生异常=' + JSON.stringify(error))
    }
    return list;
  }
}

EntryAbility.ets文件中需要【onWindowStageCreate】方法中初始化数据库,代码如下:

js 复制代码
relationalStore.getRdbStore(this.context, {
    name: RdbTools.rdbName, // 数据库文件名
    securityLevel: relationalStore.SecurityLevel.S1 // 数据库安全级别
}, (err, store) => {
    if (err) {
        Logger.error(`Failed to get RdbStore. Code:${err.code}, message:${err.message}`);
        return;
    }
    Logger.info('Succeeded in getting RdbStore.');
    //保存store, 方便后面我们对数据库的操作
    RdbTools.setStore(store)
    RdbTools.initDataBase()
});

4. 演示

然后使用真机测试即可,最终成果演示视频如下:

鸿蒙读取系统联系人

最后

  • 希望本文对你有所帮助!
  • 本人如有任何错误或不当之处,请留言指出,谢谢!
相关推荐
UnicornDev2 天前
【HarmonyOS 6】今日统计卡片实战:运动记录数据概览
华为·harmonyos·arkts·鸿蒙·鸿蒙系统
是稻香啊3 天前
HarmonyOS6 ArkTS Popup 气泡组件指南
harmonyos6
是稻香啊3 天前
HarmonyOS6 触摸目标 touch-target 属性使用指南
harmonyos6
是稻香啊4 天前
HarmonyOS6 foregroundBlurStyle 通用属性使用指南
harmonyos6
是稻香啊4 天前
HarmonyOS6 clickEffect 通用属性使用指南
harmonyos6
是稻香啊4 天前
HarmonyOS6 filter 通用属性使用指南
harmonyos6
UnicornDev7 天前
【HarmonyOS 6】个人中心数据可视化实战
华为·harmonyos·arkts·鸿蒙·鸿蒙系统
是稻香啊10 天前
HarmonyOS6 ArkUI 无障碍悬停事件(onAccessibilityHover)全面解析与实战演示
华为·harmonyos·harmonyos6
是稻香啊10 天前
HarmonyOS6 背景设置:background 基础属性全解析
harmonyos6
是稻香啊11 天前
HarmonyOS6 ArkUI 触摸拦截(onTouchIntercept)全面解析与实战演示
ubuntu·华为·harmonyos·harmonyos6