-
产品质量不仅是企业的生命线,更是用户体验的第一道门。 HUAWEI AppGallery Connect “云测+云调”双引擎,助力开发者轻松跨越从代码到产品的最后一公里! 一、云测试:真机云端护航,高效测试省成本移动应用生命周期中的测试环节,常面临成本高企、效率低下、技术门槛高等挑战。HUAWEI AppGallery Connect 云测试提供一站式解决方案:海量热门主流真机,可全面检测应用兼容性、性能、稳定性、功耗及UX等关键指标。测试完成后快速输出详尽专业的测试报告,精准定位问题并提供修复建议,显著提升应用质量与发布效率,大幅降低测试成本与资源投入。二、云调试:彻底告别"设备荒",调试效率提升50%HUAWEI AppGallery Connect云调试为开发者提供海量移动终端设备支持,解决设备机型不足、设备管理困难及bug无法复现等问题,支持7x24小时远程调测,彻底告别"设备荒"。真实运行环境精准复现用户场景,断点、日志即时获取,调试效率提升50%。用云端弹性资源替代固定资产投入,实现开发效能与成本支出双优化,让调试瓶颈迎刃而解。三、双剑合璧,节约测试成本50%,崩溃率直降72%15日天气预报应用,应用版本更新快,新版本上线时间紧迫。通过云测试全面的测试报告,开发者快速发现了不同机型上出现的无响应、UI异常、崩溃等问题,节约测试成本50%。并通过云调试快速定位,崩溃率下降了72%。(上述数据来源于合作伙伴实践反馈,具体效果以实际场景为准)AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
AppGallery_Connect
发表于2025-07-22 20:35:46
2025-07-22 20:35:46
最后回复
AppGallery_Connect
2025-07-22 20:35:46
74 0 -
在数字化浪潮下,开发者们始终在追寻更高效的开发方式。传统开发模式效率低,人力、运维、服务器等成本问题,逐渐成为开发者创新的“绊脚石”。 Cloud Foundation Kit(云开发服务)为开发者提供了鸿蒙应用/元服务高效开发的解决方案,您可按需使用云函数、云数据库、云存储、预加载等云端服务,专注于应用的创新和业务逻辑的实现,开发周期缩短50%! 一、低成本运维:从“负重前行”到“轻装上阵”云开发服务通过提供开箱即用的云函数、数据库、存储等后端核心能力,使开发者无需手动构建和管理云端资源,即可快速搭建完整的后端服务,显著提升应用功能的开发与上线效率。同时,云开发服务为各类能力配置了免费资源额度,降低新用户的试错成本和初期资源投入。 二、资源弹性伸缩, 减少闲置资源浪费云开发服务通过弹性伸缩与按量计费机制,有效解决了传统架构应对业务波峰波谷的痛点。在业务高峰时,系统可自动扩容保障稳定性;当业务量回落低谷时,资源自动收缩,避免因采购高性能服务器集群导致的资源闲置浪费,使开发者仅需为实际消耗的资源付费,显著提升资源利用率。同时,云开发服务为各类能力配置了免费资源额度, 对于测试验证阶段原型或功能简单的轻量级APP,其免费额度已能覆盖核心业务需求,大幅降低初期投入门槛。 三、端云一体化,端侧开发轻松变全栈DevEco Studio的端云一体化开发能力通过整合HarmonyOS应用/元服务的端侧与云侧开发流程,为开发者提供高效开发“快车道”——开发者仅需一套开发工具、一个开发团队,即可同时完成HarmonyOS应用/元服务的端侧与云侧开发,前端工程师轻松化身为全栈工程师,大幅缩短应用上线周期。(数据为典型场景下的参考值,实际效果会因应用复杂度等因素有所不同)AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
-
如何缩短内容与用户的触达路径,如何覆盖多样化的分发渠道并差异化运营?华为AppGallery Connect(简称AGC)向开发者推出App Linking技术服务。此服务是HarmonyOS系统层为开发者开放的统一链接能力,不仅有效提升了用户体验,而且也提升了链接转换率。案例展示:1、 哔哩哔哩,碰一碰覆盖线下内容分享,操作步骤立省60%哔哩哔哩接入App Linking与碰一碰分享服务,通过碰一碰分享B站视频。相较于传统的通信软件分享视频,操作步骤立省60%。2、美团一扫即达,服务快人一步,操作效率提升30%以上美团App接入App Linking,无需用户打开App,通过系统扫码直接解锁共享单车、租借充电宝。负一屏、控制中心、系统相机均可解锁,操作入口增加3倍。一步扫码直达,操作效率提升30%以上。 3、多乐掼蛋游戏,手机碰一碰快速闭环游戏邀请,操作步骤立省60%多乐掼蛋游戏,接入App Linking+碰一碰分享服务,实现手机碰一碰快速要求玩家加入游戏。全新的游戏邀请体验,操作便捷,趣味十足,适合宿舍、节假日家庭聚会等场景。相较于传统的通信软件分享视频,操作步骤立省60%。 鸿蒙App Linking现已助力多类型应用实现场景化链接跳转,目前该能力已开放给HarmonyOS开发者,诚邀您体验智能链接分发带来的效率革新!点击下方链接,即刻开启鸿蒙生态场景化运营新篇章——点击链接即可体验:App Linking AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一 站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
-
线下服务场景中,开发者常面临用户触达率低、推荐不精准、转化效果差等问题。传统推送方式依赖用户主动搜索或广告曝光,无法结合实时位置精准匹配需求,导致服务推荐滞后、用户体验不佳。对此,HarmonyOS为开发者准备了一种智能、低门槛、高转化的近场服务解决方案——POI场景及信标设备场景:基于用户实时位置或通过用户手机定位与信标设备联动,智能识别用户场景,并经由小艺建议等入口推荐服务,从而提升用户触达率和满意度。POI及信标类近场服务的典型特点包括:精准定位:1-200米范围内动态感知用户位置。场景化推荐:结合POI或信标设备,匹配用户当下需求。无缝体验:通过系统级入口(如小艺建议)直接触达用户,常规点选多层级步骤缩减约75%至一步直达操作。一、近场服务典型应用场景 二、实际应用案例场景一(POI):智慧文旅——效率、体验双升级,减少约80%操作步骤 游客靠近景区时,小艺建议获取购票服务卡片推荐,一键直达购票页面,相较常规点评软件的购票方式可减少约80%操作步骤; 进入景区游览时,基于景区内不同景点推荐景点介绍详情页,景点讲解、景点推荐、游览路线推荐等一目了然,再也不用边走边问。场景二(信标设备):商铺引流——坪效提升新引擎 商铺部署信标设备后,用户进入信标连接范围即可收到传输信号,通过小艺建议获取商铺热门产品、优惠活动、折扣套餐等推荐; 相较未部署信标设备的商铺依赖自然流量,信标部署商铺可主动“出击”,提前曝光吸引用户关注眼球,从而提升商铺的客流量,进而潜在提升会员转化及客单成交率。 欢迎广大鸿蒙开发者体验近场服务。如果您是一位鸿蒙开发者,赶快加入体验服务,让我们携手共进,共享鸿蒙发展的无限机遇,点击链接即可体验:POI场景、信标设备场景。AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,可扫描下方二维码或者点击此处免费咨询。 如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
-
问题背景针对App产品存在多个客户端版本的情况下,同时开发 多 个 App 时,由于业务目标、用户群体可能存在差异,且需兼顾协同效率与质量稳定性,容易暴露出比单一 App 开发更复杂的问题多产品App核心问题,本质是 “个性需求与共性能力的平衡失控”:资源分散导致效率低,协同缺失导致体验乱,版本混乱导致风险高。无 模块化架构设计,项目陷入 “开发慢、改不动、问题多” 的恶性循环。 安卓开发现状人力资源分配矛盾:若 多 个 App 并行开发,核心开发人员(如架构师、资深工程师)需同时跟进多个项目,精力被稀释,导致技术决策延迟、关键问题响应变慢。基层开发人员若按 “1 个 App 对应 1 个团队” 划分,会出现 “同一项基础功能(如图片上传、异常监控)3 个团队各做一套” 的情况,重复劳动率高达 40%-60%,直接拉长整体开发周期。技术栈与规范难统一若 多个 App 由不同团队开发,可能因 “团队习惯” 采用差异技术方案,导致后续跨 App 协作(如人员轮岗、问题排查)成本陡增即使预先制定规范,也可能因 “赶进度” 出现执行偏差(如命名规则、接口格式不统一),后期需额外投入人力做标准化整改共性能力重复开发,维护难度翻倍:多个 App 必然存在共性能力(如登录、支付、网络请求、数据埋点),若未提前抽象复用,会导致:同一功能出现 多套代码,修复一个共性 Bug(如登录接口超时逻辑)需在 3多个 App 中分别修改,漏改概率增加共性能力升级(如支付渠道新增)需 多个团队同步适配,协调成本随 App 数量呈指数级增长版本规划与测试压力陡增多 个 App 的版本迭代节奏可能不同(如 A App 需每月一更,B App 每两周一更,C App 紧急上线),测试资源(如测试设备、自动化脚本)需在 3 个项目间频繁切换,导致测试覆盖率下降,漏测风险升高。若 多 个 App 依赖同一基础组件(如自研的网络库),该组件升级后,需 多个 App 同步完成兼容性测试才能发布,任何一个 App 的测试延迟都会拖慢整体进度。线上问题连锁反应若共性能力(如埋点 SDK)存在隐藏 Bug,可能导致 多个 App 同时出现数据异常,线上故障排查时需 “多线并行定位”,定位时间比单一 App 问题长 2-3 倍。某一个 App 的紧急发布(如修复崩溃 Bug)可能因 “打包环境共享”“配置文件混淆” 影响其他 App 的发布包稳定性(如误打包旧版本代码)。业务与扩展性:差异需求失控多 个 App 的业务差异(如 A App 需社交功能,B App 需电商功能,C App 需工具功能)可能要求对共性能力做 “定制化修改”(如登录模块为 A App 新增 “第三方社交账号登录”,为 B App 新增 “手机号一键登录”),若修改未抽象成可配置逻辑,会导致共性模块逐渐 “臃肿”,最终失去复用价值 鸿蒙解决方案 整体架构设计思路: 备注:一个业务功能,即为一个工程(整个工程下的一个文件夹),编译出后是一个HAR/HSP类型的包。多个HAR/HSP组合打包出的包为HAP包。(HAR、HSP、HAP包区别参考:https://developer.huawei.com/consumer/cn/doc/architecture-guides/tools-v1_2-ts_35-0000002343405565) 在鸿蒙生态中,通过 ArkTS 语言和 ArkUI 框架的原生支持,可以高效实现 "一套工程、多 App 发布" 的架构。具体实现策略:功能模块包模块化设计:可插拔组件化开发。由组件复用提供基础能力,例如:一键加油、爱车服务、无感支付、在线订单、高德、在线商城等业务功能,每个都由HAR/HSP工程创建,实现业务功能与业务无关的网络库、埋点SDK、图片加载等,每个都由HAR/HSP工程创建,实现基础功能。由业务功能HAR/HSP包调用,为业务功能提供基础能力上述业务功能HAR/HSP包,基础功能HAR/HSP包,可自由组合,被HAP工程引入,由HAP工程打包出用户版、商户版、供应商版三个版本工程架构设计,组件复用,实现一套代码库支撑多 App用户版HAP工程打包:创建hapTasks类型的工程(运行出的包为HAP包),将多个需要的多个业务功能包( HAR/HSP工程(文件夹))引入,编码实现用户版的功能。商户版、供应商版也是如此。用户版打包:需为车主提供便捷的车辆养护、维修、紧急救援等服务,引入一键加油、爱车服务、无感支付HAR/HSP,实现相关业务逻辑后,打包成HAP包商户版打包:需帮助维修店/4S店高效管理客户和服务流程,引入在线订单、高德HAR/HSP,实现相关业务逻辑后,打包成HAP包供应商版打包:为配件供应商提供B2B销售渠道和管理工具,引入在线商城、充值相关渠道配置HAR/HSP,实现相关业务逻辑后,打包成HAP包如何打HAP包(多个app的差异化打包):。上述用户版、商户版、供应商版工程,每个工程需要配置:包名、签名、证书、打包输出的文件夹路径、相关资源(如主题资源、图片资源等)每个 HAP 的 module.json 中,bundleName、bundleType、versionCode、debug、minAPIVersion 保持一致;module 的 name 字段互不相同;minCompatibleVersionCode、targetAPIVersion 保持一致配置后,通过执行Hvigor命令,打包成HAP包(Hvigor脚本参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/packing-tool#多工程打包指令) 解决痛点模块化设计,代码复用率提升:基础组件统一维护,避免重复开发(复用率可达 70%-90%)。功能模块通过配置按需加载,无需为每个 App 单独编写代码,并且可以将可插拔的模块组合打包多个App。组件复用,开发效率提升:修改公共组件自动同步到所有 App,减少重复测试和发布流程。新增功能只需在对应模块开发,通过配置快速集成到需要的 App。维护成本降低:单一工程结构减少代码仓库管理复杂度,团队协作更高效。版本控制更简单,所有 App 基于同一代码基线演进。灵活扩展能力:新增 App 只需创建新的配置文件和专属资源,无需复制代码。功能模块可独立升级,不影响其他 App。 组件设计思路: 组件复用,设计思路:基础层组件:网络库、埋点SDK、图片加载等与业务无关联的,放入基础层,可供任何App提供底层能力业务层:支付模块、订单、商品详情等,与业务强关联,需要考虑多个App版本的不同差异化能力、公共能力,进一步抽出例如可选复用的业务功能,作为多个App集成的公共业务差异化的使用动态Feature包由多个App灵活调用,并且可设计多个Feature包,可插拔给多个App组合使用产品层:根据不同App版本,将公共资源统一管理、特定产品特定资源文件、代码中动态加载资源,封装在不同的App中每个App就固定使用这些资源、动态加载业务层的业务包,灵活配置不用App版本之间所需要的业务功能基础层、业务层功能维护: 每个基础能力、业务模块完全独立开发,无需关心是哪个App来调用,仅需关注本身能力、业务的迭代开发 基础组件案例案例1,验证码组件: import { inputMethod } from '@kit.IMEKit';import { emitter } from '@kit.BasicServicesKit';import { hilog } from '@kit.PerformanceAnalysisKit';@Extend(Text)function verifyCodeUnitStyle() { .fontSize($r("sys.float.ohos_id_text_size_body1")) .fontWeight(60) .textAlign(TextAlign.Center) .width($r("app.integer.verify_code_code_unit_with")) .height('100%') .margin({ left: $r("app.integer.verify_code_code_unit_margin"), right: $r("app.integer.verify_code_code_unit_margin") }) .border({ width: { bottom: $r("app.integer.verify_code_code_border_width") }, color: { bottom: Color.Grey }, style: { bottom: BorderStyle.Solid } })}@Componentstruct VerifyCodeComponentWithoutCursor { @State codeText: string = ""; private readonly verifyID: string = "verifyCodeComponent"; private inputController: inputMethod.InputMethodController = inputMethod.getController(); // 监听键盘弹出收起状态 @State isKeyboardShow: boolean = false; private verifyCodeLength: number = 6; private isListen: boolean = false; private textConfig: inputMethod.TextConfig = { inputAttribute: { textInputType: inputMethod.TextInputType.NUMBER, enterKeyType: inputMethod.EnterKeyType.GO }, }; private codeIndexArray: Array<number> = Array.from([0, 1, 2, 3, 4, 5]); // 注册路由返回函数,案例插件不触发 popRouter: () => void = () => { }; aboutToAppear(): void { // 注册返回监听,包括点击手机返回键返回与侧滑返回 this.listenBackPress(); } async attachAndListen(): Promise<void> { focusControl.requestFocus(this.verifyID); await this.inputController.attach(true, this.textConfig); logger.info("attached"); this.listen(); this.isKeyboardShow = true; } listenBackPress() { let innerEvent: emitter.InnerEvent = { eventId: 5 }; // 收到eventId为5的事件后执行回调函数 emitter.on(innerEvent, () => { if (this.isKeyboardShow) { // 退出文本编辑状态 this.inputController.hideTextInput(); this.isKeyboardShow = false; } else { this.popRouter(); } }); } aboutToDisappear(): void { this.off(); // 关闭事件监听 emitter.off(5); } /** * TODO 知识点:绑定输入法 */ async attach() { await this.inputController.attach(true, this.textConfig); logger.info("attached"); } /** * TODO:知识点:解绑 */ off(): void { this.inputController.off("insertText"); this.inputController.off("deleteLeft"); this.isListen = false; logger.info("detached"); // 退出文本编辑状态 this.inputController.hideTextInput(); this.isKeyboardShow = false; } /** * TODO 知识点:订阅输入法代插入、向左删除事件,从而获得键盘输入内容 */ listen() { if (this.isListen) { return; } this.inputController.on("insertText", (text: string) => { if (this.codeText.length >= this.verifyCodeLength || isNaN(Number(text)) || text === ' ') { return; } this.codeText += text; if (this.codeText.length === this.verifyCodeLength) { logger.info("VerifyCode: %{public}s", this.codeText); } logger.info("VerifyCode [insert]: %{public}s", this.codeText); }) this.inputController.on("deleteLeft", (length: number) => { this.codeText = this.codeText.substring(0, this.codeText.length - 1); logger.info("VerifyCode [delete left]: %{public}s", this.codeText); }) this.isListen = true; logger.info("listener added"); } /** * TODO 知识点:部分验证码场景要完全禁止对输入验证码的选中、复制等功能,因此可以使用Text组件完成 */ @Builder buildVerifyCodeComponent() { Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) { ForEach(this.codeIndexArray, (item: number, index: number) => { Text(this.codeText[item]) .verifyCodeUnitStyle() }, (item: number, index: number) => item.toString()) } .id(this.verifyID) /** * TODO:知识点:当可视面积变化时进行绑定注册与解绑 */ .onBlur(() => { this.off(); }) .backgroundColor(Color.Transparent) .height($r("app.integer.verify_code_verify_code_height")) .margin({ left: $r("sys.float.ohos_id_card_margin_start"), right: $r("sys.float.ohos_id_card_margin_start") }) .defaultFocus(true) .onClick(() => { // TODO 知识点:点击本组件时弹出输入法,因为这里使用的是Text组件,因此需要重新attach,而不能直接使用showSoftKeyboard this.attachAndListen(); }) } build() { Row() { this.buildVerifyCodeComponent() } }}@Builderexport function VerifyCodeViewBuilder() { VerifyCodeView()}/** * 验证码组件:禁用选中、复制、光标 */@Componentexport struct VerifyCodeView { popRouter: () => void = () => { }; build() { NavDestination(){ Column() { VerifyCodeComponentWithoutCursor({ popRouter: this.popRouter }) } .height('100%') .width('100%') .justifyContent(FlexAlign.Center) } .title('验证码界面') }}/** * 日志打印类 */class Logger { private domain: number; private prefix: string; private format: string = '%{public}s, %{public}s'; constructor(prefix: string) { this.prefix = prefix; this.domain = 0xFF00; this.format.toUpperCase(); } debug(...args: string[]) { hilog.debug(this.domain, this.prefix, this.format, args); } info(...args: string[]) { hilog.info(this.domain, this.prefix, this.format, args); } warn(...args: string[]) { hilog.warn(this.domain, this.prefix, this.format, args); } error(...args: string[]) { hilog.error(this.domain, this.prefix, this.format, args); }}export let logger = new Logger('[CommonAppDevelopment]') 案例2,地址选择器组件: import { window } from '@kit.ArkUI';import { AddressInfo, AddressType, CommonAddressList, Location, Province } from '../model/AddressModel';import { JsonUtils } from '../utils/JsonUtils';/** * 常量 */export default class Constants { // 自定义TabBar切换tab动画分隔线宽度 public static readonly DIVIDER_WIDTH: number = 20; // 顶部省市区间隔 public static readonly AREA_SPACE: number = 12; // rawfile目录下的省市区json文件 public static readonly JSON_FILE: string = 'address';}/** * 自定义地址选择组件CustomAddressPicker */@Componentexport struct CustomAddressPicker { // 底部导航条区域高度 @State bottomHeight: number = 0; // 选择的省市区 @State provinceCityRegion: string = '省、市、区'; // 用于对外提供选择后的省市区信息或者传入地址信息 @Link address: AddressInfo; // 地址选择半模态弹窗显隐标志位 @State isShow: boolean = false; // 当前选择的省、市、区tab页签的index。0表示省,1表示市,2表示区 @State currentIndex: number = AddressType.Province; // 调用changeIndex切换TabContent动画时长 @State animationDuration: number = 300; // 省List @State provinceList: CommonAddressList[] = []; // 市List @State cityList: CommonAddressList[] = []; // 区List @State regionList: CommonAddressList[] = []; // 记录上一次市List @State lastCityList: CommonAddressList[] = []; // 记录上一次区List @State lastRegionList: CommonAddressList[] = []; // 存放选择的省数据 @State province: Province = new Province('', '', []); // 记录当前省市区选择信息 @State currentSelectInfo: AddressInfo = new AddressInfo(); // 记录上一次省市区选择信息 @State lastSelectInfo: AddressInfo = new AddressInfo(); // 选择的省市区名下方的下滑线水平偏移量 @State leftMargin: number = 0; // 存放上一次选择的省市区名下方的下滑线水平偏移量 private lastLeftMargin: number = 0; // 存放选择的省市区名下方的下滑线位置信息 private textInfos: [number, number][] = []; // 存放从json读取的省市区数据 private data: Province[] = []; private controller: TabsController = new TabsController(); async aboutToAppear() { // 获取导航条高度,半模态弹窗内容进行避让 window.getLastWindow(getContext(), (err, data) => { const avoidAreaBottom = data.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) this.bottomHeight = avoidAreaBottom.bottomRect.height }) // 从json文件读取省市区数据 const addressData: Province[] = await JsonUtils.getAddressJson(Constants.JSON_FILE) if (!addressData || addressData.length === 0) { console.error('省市区数据加载失败'); return; } for (let index = 0; index < addressData.length; index++) { this.data.push(addressData[index]) this.provinceList.push(new CommonAddressList(addressData[index].code, addressData[index].name)); } // 首次加载AddressPickerComponent如果传入了有效的地址信息,拉起地址选择半模态页面时,会按传入的地址信息进行显示 this.initAddressSelect() } /** * 首次加载AddressPickerComponent如果传入了有效的地址信息时,在拉起地址选择半模态页面时,会按传入的地址信息进行显示 */ initAddressSelect() { if (this.address.province !== '' && this.address.city !== '' && this.address.region !== '') { this.provinceCityRegion = this.address.province + this.address.city + this.address.region this.currentSelectInfo.province = this.address.province this.currentSelectInfo.city = this.address.city this.currentSelectInfo.region = this.address.region //查找对应的市,区地址信息 this.data.forEach(province => { if (province.name === this.address.province) { this.currentSelectInfo.provinceId = province.code; this.address.provinceId = province.code; province.children.forEach(city => { // 只提取市级的code和name this.cityList.push(new CommonAddressList(city.code, city.name)) if (city.name === this.address.city) { this.currentSelectInfo.cityId = city.code this.address.cityId = city.code city.children.forEach(region => { // 只提取区级的code和name this.regionList.push(new CommonAddressList(region.code, region.name)) if (region.name === this.address.region) { this.currentSelectInfo.regionId = region.code this.address.regionId = region.code // 深拷贝保存到相应的变量中 this.lastSelectInfo = JSON.parse(JSON.stringify(this.currentSelectInfo)) this.lastCityList = JSON.parse(JSON.stringify(this.cityList)); this.lastRegionList = JSON.parse(JSON.stringify(this.regionList)); this.animationDuration = 0; } }) } }) } }) } } /** * 选择的省市区名下方的下滑线动画 * @param duration 动画时长 * @param leftMargin 下划线动画偏移量 */ startAnimateTo(duration: number, leftMargin: number) { animateTo({ duration: duration, // 动画时长 curve: Curve.Linear, // 动画曲线 iterations: 1, // 播放次数 playMode: PlayMode.Normal // 动画模式 }, () => { this.leftMargin = leftMargin; }) } /** * 用于显示选择的省、市、区名 * @param params 传入要显示的省、市、区名 */ @Builder locationItem(params: Location) { Text(params.name === '' ? "请选择" : params.name) .height("100%") .fontSize(16) .fontWeight(this.currentIndex === params.index ? 500 : 400) .fontColor(this.currentIndex === params.index ? "#cc000000" : "#ff8d8d8d") .constraintSize({ maxWidth: "33%" }) .textOverflow({ overflow: TextOverflow.Ellipsis }) .maxLines(1) .margin({ right: 12 }) .onClick(() => { this.controller.changeIndex(params.index) }) .id(params.index.toString()) .onAreaChange((oldValue: Area, newValue: Area) => { //使用组件区域变化回调onAreaChange获取选择的省市区Text组件宽度,存入textInfos数组,用于后续计算选择省市区名后下方下滑线动画水平偏移量leftMargin // 组件区域变化时获取当前Text的宽度newValue.width和x轴相对位置newValue.position.x this.textInfos[params.index] = [newValue.position.x as number, newValue.width as number]; if (this.currentIndex === params.index && params.index === AddressType.Province) { // 计算选择的省市区名下方的下滑线偏移量 this.leftMargin = (this.textInfos[this.currentIndex][1] - 20) / 2 } }) } @Builder customTabs() { Tabs({ controller: this.controller }) { // 省列表 TabContent() { List() { ForEach(this.provinceList, (item: CommonAddressList) => { ListItem() { this.areaNameItem(AddressType.Province, item) } .onClick(() => { // 如果当前点击选择的省与之前选择一样,跳过省、市数据获取,直接调用changeIndex(AddressType.City)切换到市列表,减少冗余查询以提升性能 if (this.currentSelectInfo.province == item.name) { this.controller.changeIndex(AddressType.City) return } else { // 重置市和区数据 this.currentSelectInfo.cityId = ''; this.currentSelectInfo.city = ''; this.currentSelectInfo.regionId = ''; this.currentSelectInfo.region = ''; } this.cityList = [] this.regionList = [] this.data.forEach(province => { if (province.name === item.name) { this.province = JSON.parse(JSON.stringify(province)); province.children.forEach(city => { this.cityList.push(new CommonAddressList(city.code, city.name)); }) } }) this.currentSelectInfo.provinceId = item.code; this.currentSelectInfo.province = item.name; this.controller.changeIndex(AddressType.City) }) }, (item: CommonAddressList) => JSON.stringify(item)) } .width("100%") .height("100%") .scrollBar(BarState.Off) .friction(0.6) // 设置摩擦系数 .edgeEffect(EdgeEffect.Spring) // 边缘效果设置为Spring .listDirection(Axis.Vertical) // 排列方向 } // 市列表 TabContent() { List() { ForEach(this.cityList, (item: CommonAddressList) => { ListItem() { this.areaNameItem(AddressType.Province, item) } .onClick(() => { // 如果点击的市和上一次点击的市一样,则不用刷新,减少冗余操作以提升性能 if (this.currentSelectInfo.city === item.name) { this.controller.changeIndex(AddressType.Region) return } else { //重置数据 this.currentSelectInfo.region = '' this.currentSelectInfo.regionId = '' } this.regionList = [] // 点击市,获取该市所有区,存入regionList this.province.children.forEach(city => { if (city.name === item.name) { city.children.forEach(region => { this.regionList.push(new CommonAddressList(region.code, region.name)) }) } }) this.currentSelectInfo.cityId = item.code this.currentSelectInfo.city = item.name this.controller.changeIndex(AddressType.Region) }) }, (item: CommonAddressList) => JSON.stringify(item)) } .width("100%") .height("100%") .scrollBar(BarState.Off) .friction(0.6) .edgeEffect(EdgeEffect.Spring) .listDirection(Axis.Vertical) } // 区列表 TabContent() { List() { ForEach(this.regionList, (item: CommonAddressList) => { ListItem() { this.areaNameItem(AddressType.Province, item) } .onClick(() => { // 记录选择的区信息 this.currentSelectInfo.regionId = item.code; this.currentSelectInfo.region = item.name; this.provinceCityRegion = this.currentSelectInfo.province + this.currentSelectInfo.city + this.currentSelectInfo.region //退出半模态 this.isShow = false // 将当前选中省市区信息保存到lastSelectInfo this.lastSelectInfo.provinceId = this.currentSelectInfo.provinceId; this.lastSelectInfo.province = this.currentSelectInfo.province; this.lastSelectInfo.cityId = this.currentSelectInfo.cityId; this.lastSelectInfo.city = this.currentSelectInfo.city; this.lastSelectInfo.regionId = this.currentSelectInfo.regionId; this.lastSelectInfo.region = this.currentSelectInfo.region; // 在选择完区名后,使用JSON.parse(JSON.stringify(xxx))深拷贝选择的省市区数据,用于后续操作中需要加载上一次选择的完整省市区数据 // 深拷贝保存到相应的变量中 this.lastCityList = JSON.parse(JSON.stringify(this.cityList)); this.lastRegionList = JSON.parse(JSON.stringify(this.regionList)); this.address = JSON.parse(JSON.stringify(this.lastSelectInfo)); }) }, (item: CommonAddressList) => JSON.stringify(item)) } .width("100%") .height("100%") .scrollBar(BarState.Off) .friction(0.6) .edgeEffect(EdgeEffect.Spring) .listDirection(Axis.Vertical) } } .onAppear(() => { if (this.lastSelectInfo.region !== '') { // 上一次选择如果选择到区,再次打开半模态弹窗页面时会显示到区的TabContent this.currentIndex = AddressType.Region; if (this.cityList.length === 0 && this.regionList.length === 0) { // 在已经选择过省市区后,再次打开地址选择半模态弹窗页面,但是没有选择到区就关闭了半模态页面,此时如果再次打开半模态页面,需要显示之前完整选择的省区市数据 this.currentSelectInfo.provinceId = this.lastSelectInfo.provinceId; this.currentSelectInfo.cityId = this.lastSelectInfo.cityId; this.currentSelectInfo.regionId = this.lastSelectInfo.regionId; this.currentSelectInfo.province = this.lastSelectInfo.province; this.currentSelectInfo.city = this.lastSelectInfo.city; this.currentSelectInfo.region = this.lastSelectInfo.region; this.cityList = JSON.parse(JSON.stringify(this.lastCityList)); this.regionList = JSON.parse(JSON.stringify(this.lastRegionList)); this.leftMargin = this.lastLeftMargin; } else { this.leftMargin = this.textInfos[0][1] + this.textInfos[1][1] + (this.textInfos[2][1] - 20) / 2 + 12 * 2 this.lastLeftMargin = this.leftMargin; } this.controller.changeIndex(AddressType.Region) } this.animationDuration = 300 }) .onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => { if (index === targetIndex) { return; } this.currentIndex = targetIndex; let leftMargin: number = 0; let isAnimating: boolean = false; if (index === AddressType.Province && targetIndex === AddressType.City) { // 从省切到市时,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = this.textInfos[0][1] + (this.textInfos[1][1] - Constants.DIVIDER_WIDTH) / 2 + Constants.AREA_SPACE; isAnimating = this.currentSelectInfo.city === '' ? false : true; } else if (index === AddressType.City && targetIndex === AddressType.Region) { // 从市切到区,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = this.textInfos[0][1] + this.textInfos[1][1] + (this.textInfos[2][1] - Constants.DIVIDER_WIDTH) / 2 + Constants.AREA_SPACE * 2; isAnimating = this.currentSelectInfo.region === '' ? false : true; } else if (index === AddressType.City && targetIndex === AddressType.Province) { // 从市切到省,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = (this.textInfos[0][1] - Constants.DIVIDER_WIDTH) / 2; isAnimating = this.currentSelectInfo.city === '' ? false : true; } else if (index === AddressType.Region && targetIndex === AddressType.City) { // 从区切到市,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = this.textInfos[0][1] + (this.textInfos[1][1] - Constants.DIVIDER_WIDTH) / 2 + Constants.AREA_SPACE; isAnimating = this.currentSelectInfo.region === '' ? false : true; } else if (index === AddressType.Region && targetIndex === AddressType.Province) { // 点击自定义TabBar从区切到省,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = (this.textInfos[0][1] - Constants.DIVIDER_WIDTH) / 2; isAnimating = this.currentSelectInfo.region === '' ? false : true; } else if (index === AddressType.Province && targetIndex === AddressType.Region) { // 点击自定义TabBar从省切到区,重新计算选择的省市区名下方的下滑线偏移量 leftMargin = this.textInfos[0][1] + this.textInfos[1][1] + (this.textInfos[2][1] - Constants.DIVIDER_WIDTH) / 2 + Constants.AREA_SPACE * 2; isAnimating = this.currentSelectInfo.region === '' ? false : true; } // 只有在已经选择过的TabContent之间切换时,才会做下划线水平偏移动画 if (isAnimating) { this.startAnimateTo(this.animationDuration, leftMargin); } else { this.leftMargin = leftMargin; } }) .width("100%") .barHeight(0) .layoutWeight(1) } /** * 自定义省/市/区名项 * @param addressType 省/市/区类型 * @param item 省、市、区地址项 */ @Builder areaNameItem(addressType: AddressType, item: CommonAddressList) { Column() { Text(item.name) .width("90%") .height(48) .fontSize(16) .fontColor(this.getFontColor(addressType, item)) Divider().width("90%") .strokeWidth(1) .color("#F1F3F5") } .width("100%") } /** * 获取省、市、区名需要显示的字体颜色 * @param addressType 省/市/区类型 * @param item 省、市、区地址项 * @returns 需要显示的字体颜色 */ getFontColor(addressType: AddressType, item: CommonAddressList): Color | string | Resource { // 省/市/区名字体颜色 let isSelect: boolean = false; if (addressType === AddressType.Province) { isSelect = this.currentSelectInfo.province !== '' && item.name === this.currentSelectInfo.province; } else if (addressType === AddressType.City) { isSelect = this.currentSelectInfo.city !== '' && item.name === this.currentSelectInfo.city; } else if (addressType === AddressType.Region) { isSelect = this.currentSelectInfo.region !== '' && item.name === this.currentSelectInfo.region; } const color = isSelect ? "#fffcb850" : Color.Black; return color; } /** * 地址选择半模态弹窗页面 */ @Builder addressSelectPage() { Column() { this.customTabBar() Divider().width("90%") .strokeWidth(1) .color("#F1F3F5") this.customTabs() } .width("100%") .height("100%") .backgroundColor(Color.White) .padding({ bottom: this.bottomHeight + 'px' }) } /** * 自定义TabBar */ @Builder customTabBar() { RelativeContainer() { Row() { //选择的省名 this.locationItem({ index: AddressType.Province, name: this.currentSelectInfo.province }) // 选择的市名 this.locationItem({ index: AddressType.City, name: this.currentSelectInfo.city }) // 选择的区名 this.locationItem({ index: AddressType.Region, name: this.currentSelectInfo.region }) } .width("85%") .height("80%") .alignRules({ center: { anchor: '__container__', align: VerticalAlign.Center } }) .margin({ bottom: 10 }) .padding({ left: 20, top: 15 }); // 选择的省市区名下方的下滑线 Row() { Divider() .width(20) .strokeWidth(2) .color("#fffcb850") .margin({ left: this.leftMargin }) } .alignItems(VerticalAlign.Top) .width("85%") .height("20%") .alignRules({ bottom: { anchor: '__container__', align: VerticalAlign.Bottom } }) .padding({ left: 20 }) Row() { Image($r("app.media.address_picker_close")) .objectFit(ImageFit.Contain) .width(14) .height(14) .margin({ left: 20 }); } .height("100%") .width("15%") .alignRules({ right: { anchor: '__container__', align: HorizontalAlign.End } }) .onClick(() => { //关闭半模态 this.isShow = false; }); } .width("100%") .height(48) } build() { Column() { Row() { Text("所在地区") .fontSize(16) .fontWeight(500) .margin({ right: 20 }) Text(this.provinceCityRegion) .fontSize(15) .fontColor(this.provinceCityRegion === '省、市、区' ? "#ffacacac" : Color.Black) .fontWeight(300) .constraintSize({ maxWidth: "68%" }) .textOverflow({ overflow: TextOverflow.Ellipsis }) .maxLines(1) } .width("100%") .height(100) .onClick(() => { this.isShow = true this.currentIndex = AddressType.Province }) .bindSheet($$this.isShow, this.addressSelectPage(), { height: "70%", showClose: false, // 设置不显示自带的关闭图标 dragBar: false, onDisappear: () => { this.animationDuration = 0; if (this.currentSelectInfo.region === '') { // 重置所有状态 this.currentSelectInfo.provinceId = ''; this.currentSelectInfo.cityId = ''; this.currentSelectInfo.regionId = ''; this.currentSelectInfo.province = ''; this.currentSelectInfo.city = ''; this.currentSelectInfo.region = ''; this.cityList = []; this.regionList = []; } } }) } .width("100%") .height(54) .padding(2) }}核心能力:组件复用 总结鸿蒙一多开发统一工程与模块化架构,解决人力资源分配矛盾与重复劳动问题。依托标准化组件复用,确保共性能力集中维护,解决功能重复开发、维护难的问题。组件化与配置化打包让测试聚焦差异点,缓解版本规划压力与测试资源冲突。统一资源管理与编译脚本精准控制打包,降低线上问题连锁反应概率,保障多 App 发布稳定性。
-
问题背景在鸿蒙App开发中,调用鸿蒙定位服务API获取的当前定位坐标后,传入华为地图后,在华为地图上显示的定位坐标,与实际预期的定位位置不一样例如:鸿蒙定位服务API获取的当前定位坐标,预期在华为地图上应该显示在湖附近,但是实际华为地图上显示的位置,在几百米外的陆地上。具体效果见下面截图即,应用内通过鸿蒙定位服务API获取的当前定位坐标,与华为地图中显示的坐标位置存在偏差 问题原因 鸿蒙定位服务API使用的是WGS84坐标系,但是在显示到华为地图上需要使用GCJ02 坐标系华为地图坐标系介绍:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/map-introduction 鸿蒙定位服务API坐标系介绍:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/location-guidelines 问题原因总结华为官方设计上存在不一致:华为地图需要使用标准的大陆的GCJ02 坐标系,与鸿蒙定位服务API的WGS84坐标系,设计上不一致 修复方案:核心答案封装一套坐标系转换的方法,将WGS84坐标系的坐标转换为GCJ02坐标系的坐标实现步骤鸿蒙原生通过鸿蒙定位服务API获取到坐标后,调用封装的经纬度坐标系转换方法,将转换后的坐标,传入到华为地图中显示经纬度坐标转换方法,见如下代码设计思路 经纬度坐标转换,代码设计思路 先定义一个接受经度、纬度两个参数的方法,并返回number数组,如下:gcj02ToWgs84(lng: number, lat: number)判断是否为国内坐标,若是则继续转化,否则退出封装一个转换经度的方法,如下:transformLng封装一个转换纬度的方法,如下:transformLat再经过固定算法,在gcj02ToWgs84返回number数组 完整代码getAddressPermission() { //位置权限 let atManager = abilityAccessCtrl.createAtManager(); console.log('requestPermissionsFromUser' + 1) try { atManager.requestPermissionsFromUser(getContext(), ['ohos.permission.INTERNET', 'ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION']) .then((data) => { console.log('requestPermissionsFromUser' + JSON.stringify(data)) try { geoLocationManager.getCurrentLocation(request) .then((result) => { // 调用getCurrentLocation获取当前设备位置,通过promise接收上报的位置 console.info('current location: ' + JSON.stringify(result)); // 通过wgs84ToGcj02转换为gcj02坐标 const lngLat = wgs84ToGcj02(result.longitude, result.latitude) setTimeout(() => { this.setMark(result.longitude, result.latitude, "位置(wgs84,位置偏移)", $r("app.media.position")) this.setMark(lngLat[0], lngLat[1], "位置(gcj02,位置准确)", $r("app.media.position")) }, 1000) }) .catch((error: BusinessError) => { // 接收上报的错误码 console.error('promise, getCurrentLocation: error=' + JSON.stringify(error)); }); } catch (err) { console.error("errCode:" + JSON.stringify(err)); } }) .catch((err: BusinessError) => { console.log('requestPermissionsFromUser' + 3) // Logger.error(TAG, `err: ${JSON.stringify(err)}`); }) } catch (err) { console.log('requestPermissionsFromUser' + 4) } } const PI = Math.PI;const a = 6378245.0;const ee = 0.00669342162296594323;function outOfChina(lng: number, lat: number): boolean { if (lng < 72.004 || lng > 137.8347) { return true; } if (lat < 0.8293 || lat > 55.8271) { return true; } return false;}function transformLat(lng: number, lat: number): number { let ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0; ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0; return ret;}function transformLng(lng: number, lat: number): number { let ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0; ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0; return ret;}function gcj02ToWgs84(lng: number, lat: number): number[] { if (outOfChina(lng, lat)) { return [lng, lat]; } let dlat = transformLat(lng - 105.0, lat - 35.0); let dlng = transformLng(lng - 105.0, lat - 35.0); let radlat = lat / 180.0 * PI; let magic = Math.sin(radlat); magic = 1 - ee * magic * magic; let sqrtmagic = Math.sqrt(magic); dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI); dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI); let mglat = lat + dlat; let mglng = lng + dlng; return [lng * 2 - mglng, lat * 2 - mglat];}总结鸿蒙地图相关开发中,若存在应用内app获取定位后,需要在华为地图中显示定位坐标位置,则需要转换坐标。开发者需了解鸿蒙中此种经纬度坐标系,不同标准。若遇到类似问题,可快速解决,无需查阅很多资料花费较多时间来定位此种类型的问题若遇到类似定位相关的问题,查阅鸿蒙官网API时,需留意坐标系相关的说明,可快速定界出是否是坐标系的问题
-
使用DevEcoStudio 开发、编译鸿蒙 NEXT_APP 以及使用中文插件 #鸿蒙开发工具 #DevEco Studio1. 概述DevEco Studio 是华为提供的一站式集成开发环境(IDE),专为鸿蒙操作系统(HarmonyOS Next)应用和服务开发设计 DevEco Studio,掌握基本操作和开发流程。2. 安装与配置访问华为开发者联盟官网 下载 DevEco Studio 安装包运行安装程序,按照向导完成安装启动 DevEco Studio,首次启动会自动下载并安装 SDK 组件3. 创建新项目3.1 启动向导启动 DevEco Studio,选择 Start a new HarmonyOS project选择应用模板(如 Empty Ability、Native C++ 等)3.2 项目结构创建完成后,DevEco Studio 会生成标准的鸿蒙应用项目结构:myapplication/ ├── AppScope/ │ ├── resources/ │ └── app.json5 # 版本号等内容 ├── entry/ # 主模块 │ ├── src/ │ │ ├── main/ │ │ │ ├── ets/ # ArkTS 代码 │ │ │ ├── resources/ # 资源文件 │ │ │ └── module.json5 │ │ ├── test/ # 测试代码 │ │ ├── mock/ │ │ ├── ohosTest/ │ │ ├── build-profile.json5 │ │ ├── hvigorfile.ts │ │ ├── obfuscation-rules.txt │ │ └── oh-package.json5 │ └── build/ # 编译后测试包 ├── hvgor/ │ └── hvigor-config.json5 ├── build/ # 编译后正式包 └── build-profile.json5 # 项目构建配置文件4 代码方式布局使用 ArkTS 声明式语法:@Entry @Component export struct Index { @State message: string = 'Hello World' build() { Column() { Text(this.message) .fontSize(50) .fontWeight(FontWeight.Bold) Button('Click Me') .onClick(() => { this.message = 'Hello HarmonyOS!' }) } .width('100%') } } 5. 应用调试5.1 模拟器调试点击工具栏中的 Tools > Device Manager 图标在弹出窗口中点击 New Emulator选择设备类型和系统版本,点击 Next 完成创建点击工具栏中的运行按钮,选择模拟器启动应用5.2 真机调试用 USB 线将鸿蒙设备连接到电脑在设备上多次点击版本号开启 开发者选项 和 USB 调试 功能在 DevEco Studio 中选择设备并点击运行按钮,选择已连接的设备如设备未识别,检查 USB 驱动是否正确安装6. 代码编辑与辅助功能6.1 代码补全DevEco Studio 支持智能代码补全,输入代码时会自动提示可能的选项:输入组件名称时提示可用组件输入属性名时提示可用属性支持方法参数提示和类型检查6.2 代码导航Ctrl + 左键点击:跳转到变量、方法或类的定义处Shift + Shift:快速打开文件Alt + 左/右箭头:在编辑历史中导航7. 构建与打包7.1 构建应用点击 Build > Build Hap(s)/App(s) 构建 APP 包构建完成后,APK 文件位于 build/outputs/default 目录下7.2 签名配置点击 File > Project Structure > Project > Signing Configs选择已有的签名密钥配置签名信息并生成签名8. 中文插件切换中文版不是选择语言,而是需要下载插件点击File > Settings > Plugins搜索Chinese(Simplified)下载并应用重启 DevEcoStudio9. 常见问题与解决方法9.1 模拟器无法启动尝试删除并重新创建模拟器降低模拟器系统版本检查内存9.2 代码报错 “arkts-no-props-by-index”错误原因:ArkTS 不支持通过索引访问对象属性解决方法:使用点号表示法或 Map 结构替代
-
鸿蒙Next实现瀑布流布局 #鸿蒙影音娱乐类应用 #拍摄美化 #HarmonyOS一、环境准备与项目创建在开始实现瀑布流布局前,需确保已安装好 DevEco Studio,且已配置好鸿蒙开发环境。打开 DevEco Studio,新建一个鸿蒙应用项目,选择合适的模板(如 Empty Feature Ability),设置项目名称、包名等信息,完成项目创建。二、布局设计思路鸿蒙 Next 的瀑布流布局可以通过自定义组件结合 Column、Row 等容器组件实现。其核心思路是将数据分成若干列,每列独立滚动展示,且根据数据项高度动态调整布局,以达到类似瀑布自然流动的效果。三、基础实现创建一个自定义组件 MasonryLayout,接收图片数据数组作为参数,并根据列数将数据分配到不同列中展示:@Component export struct MasonryLayout { @Prop data: string[]; @State cols: number[] = Array.from<number>({ length: 2 }).fill(0); build() { Row({}) { ForEach(this.cols, (_col: number, cIndex) => { Column({ }) { ForEach(this.data, (item: string, i) => { if(i % this.cols.length === cIndex) { Image(item).width(`${100 / this.cols.length}%`); } }) } }) }.alignItems(VerticalAlign.Top) } } 四、引用 MasonryLayout 瀑布流组件build() { MasonryLayout({ data: ["img1.png", "img2.png", "img3.png", "img4.png", "img5.png"], }); } 五、优化与扩展1. 响应式布局通过 MediaQuery 组件根据屏幕宽度动态调整瀑布流的列数,以适配不同设备:在 UIAbility 的 onWindowStageCreate 生命周期回调中,通过窗口对象获取启动时的应用窗口宽度并注册回调函数监听窗口尺寸变化。将窗口尺寸的长度单位由 px 换算为 vp 后,即可基于前文中介绍的规则得到当前断点值,此时可以使用状态变量记录当前的断点值方便后续使用MainAbility.tsimport { window, display } from "@kit.ArkUI"; import { UIAbility } from "@kit.AbilityKit"; export default class MainAbility extends UIAbility { private windowObj?: window.Window; private col: number = 2; //... // 根据当前窗口尺寸更新断点 private updateBreakpoint(windowWidth: number): void { // 将长度的单位由px换算为vp let windowWidthVp = windowWidth / display.getDefaultDisplaySync().densityPixels; let col: number = this.col; if (windowWidthVp < 320) { // "xs"; col = 1; } else if (windowWidthVp < 600) { // "sm"; col = 2; } else if (windowWidthVp < 840) { // "md"; col = 3; } else { // "lg"; col = 4; } if (this.col !== col) { this.col = col; } } onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.getMainWindow().then((windowObj) => { this.windowObj = windowObj; // 获取应用启动时的窗口尺寸 this.updateBreakpoint(windowObj.getWindowProperties().windowRect.width); // 注册回调函数,监听窗口尺寸变化 windowObj.on("windowSizeChange", (windowSize) => { this.updateBreakpoint(windowSize.width); }); }); // ... } //... } MasonryLayout.etsinterface IBpMapCol { xs: number; sm: number; md: number; lg: number; } const bpMapCol = new Map<string, number>(); bpMapCol.set('xs', 1) bpMapCol.set('sm', 2) bpMapCol.set('md', 3) bpMapCol.set('lg', 4) @Component export struct MasonryLayout { @StorageProp('currentBreakpoint') curBp: keyof IBpMapCol = 'sm'; @Prop data: string[]; @State cols: number[] = Array.from<number>({ length: bpMapCol.get(this.curBp) || 2 }).fill(0); build() { Row({}) { ForEach(this.cols, (_col: number, cIndex) => { Column({ }) { ForEach(this.data, (item: string, i) => { if(i % this.cols.length === cIndex) { Image(item).width(`${100 / this.cols.length}%`); Text(this.curBp) } }) } }) }.alignItems(VerticalAlign.Top) } } 注:鸿蒙 next 中无法使用索引访问对象属性,如 const obj = { a: 1 } 无法使用 obj[a],这种情况可以用 Map2. 动态加载数据为了实现类似真实瀑布流不断加载新数据的效果,可以结合鸿蒙的 LazyForEach 组件,在滚动到列表底部时触发数据加载逻辑六、网络权限// config.json { "module": { "reqPermissions": [ { "name": "ohos.permission.INTERNET", "reason": "需要网络权限来加载图片" } ] } } 七、常见问题与解决方案1. 图片加载后布局跳动解决方案:使用预估高度占位,图片加载完成后更新高度2. 大数据量性能问题解决方案:实现虚拟列表,只渲染可视区域内的元素3. 滚动卡顿解决方案:使用防抖/节流处理滚动事件避免在滚动回调中执行复杂计算使用鸿蒙的 Canvas 组件替代部分布局组件4. 不同设备适配问题解决方案:使用响应式布局动态调整列数基于设备类型设置不同的默认列数八、最佳实践总结优先使用固定高度:如果业务场景允许,尽量使用固定高度或宽高比,减少动态测量开销合理实现懒加载:对于非首屏内容或图片资源,一定要实现懒加载渐进式增强体验:先确保基础功能可用,再添加动画和交互效果测试与优化:在不同设备上测试性能表现,针对卡顿问题进行专项优化遵循鸿蒙设计规范:保持与鸿蒙系统一致的视觉风格和交互体验
-
鸿蒙NEXT上传图片功能PhotoViewPicker核心功能解析 #ArkTS#鸿蒙Next#HarmonyOS_SDK应用服务#HarmonyOS 语言PhotoViewPicker 是鸿蒙系统中用于媒体资源选择的核心组件,通过它可以便捷地实现图片、视频等媒体文件的选择功能。下面从基本用法、参数配置到高级应用进行全面解析:一、PhotoViewPicker 基础用法PhotoViewPicker 的使用流程主要分为三步:创建实例配置参数启动选择器获取结果以下是最基本的使用示例:import photoAccessHelper from "@ohos.photoAccess.photoAccessHelper"; async function pickImage() { try { // 1. 创建选择器实例 const picker = photoAccessHelper.createPhotoViewPicker(); // 2. 配置选择参数(此处使用默认配置) const options = { MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE, // 只选择图片 maxSelectNumber: 1, // 最多选择张数 isSearchSupported: true, // 显示搜索 isPhotoTakingSupported: true, // 支持拍照 recommendationOptions: true, // 智能推荐 preselectedUris: true, // 预览文件 isPreviewForSingleSelectionSupported: true, // 单选是否支持预览 }; // 3. 启动选择器并获取结果 const selectedAssets = await picker.select(options); if (selectedAssets.photoUris.length) { // 处理选择的资源 console.info("选择的资源:", selectedAssets.photoUris); } } catch (error) { console.error("选择图片失败:", error); } } 二、关键参数详解PhotoViewPicker 的参数配置非常灵活,可以根据需求定制选择器的行为:const options = { MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE, // 只选择图片 maxSelectNumber: 1, // 最多选择张数 isSearchSupported: true, // 显示搜索 isPhotoTakingSupported: true, // 支持拍照 recommendationOptions: true, // 智能推荐 preselectedUris: true, // 预览文件 isPreviewForSingleSelectionSupported: true, // 单选是否支持预览 }; 三、MIMEType 参数深度解析MIMEType 参数是 PhotoViewPicker 中非常强大的一个配置项,它允许你精确控制选择器中显示的文件类型:// 示例1:只允许选择JPEG和PNG图片 IMAGE_TYPE = 'image/*', // 示例2:只允许选择视频文件 VIDEO_TYPE = 'video/*', // 示例3: MOVING_PHOTO_IMAGE_TYPE = 'image/movingPhoto' // 示例4:允许所有 IMAGE_VIDEO_TYPE = '*/*', 四、处理选择结果选择器返回的结果是一个 PhotoSelectResult 对象,包含以下关键属性:class PhotoSelectResult { /** * The uris for the selected files. * * @type { Array<string> } * @syscap SystemCapability.FileManagement.PhotoAccessHelper.Core * @since 10 */ /** * The uris for the selected files. * * @type { Array<string> } * @syscap SystemCapability.FileManagement.PhotoAccessHelper.Core * @atomicservice * @since 11 */ /** * The uris for the selected files. * * @type { Array<string> } * @syscap SystemCapability.FileManagement.PhotoAccessHelper.Core * @crossplatform * @atomicservice * @since 12 */ photoUris: Array<string>; /** * Original option. * * @type { boolean } * @syscap SystemCapability.FileManagement.PhotoAccessHelper.Core * @since 10 */ /** * Original option. * * @type { boolean } * @syscap SystemCapability.FileManagement.PhotoAccessHelper.Core * @atomicservice * @since 11 */ /** * Original option. * * @type { boolean } * @syscap SystemCapability.FileManagement.PhotoAccessHelper.Core * @crossplatform * @atomicservice * @since 12 */ isOriginalPhoto: boolean; } 获取到资源后,通常需要进行以下处理:使用文件 URI 读取文件内容进行必要的格式转换(如压缩图片)上传到服务器或保存到本地// 这里以oss上传为例 // uri为选择图片路径 import { http } from '@kit.NetworkKit'; import fs from '@ohos.file.fs' import { request } from '@kit.BasicServicesKit'; export async function upload (uri: string): Promise<string> { // 此处从服务器获取 const data = { policy: 'qweqwe', signature: 'qweqwe', ossAccessKeyId: 'qweqwe', host: 'https://???/api', }; const name = Date.now() + '.' + uri.split('.').pop(); const key = `${new Date().getFullYear()}/${name}` const context = getContext(); // 通过getContext获取沙箱地址 const destPath = `${context.cacheDir}/${name}`; const file = fs.openSync(uri); // 将文件复制一份到沙箱缓存地址,这一步尤为重要,只有沙箱地址的文件可以进行上传操作 fs.copyFileSync(file.fd, destPath); const result = await request.uploadFile(context,{ // files字段是上传的文件组成的列表,类型为formData files: [{ filename: 'file', name: 'file', uri: `internal://cache/${name}`, type: 'image/jpeg' }], // data字段是携带的参数,建议把类型设置为formData data: [ { name: 'name', value: `${name}`, }, { name: 'policy', value: `${data.policy}` }, { name: 'OSSAccessKeyId', value: Object.values(data)[2] }, { name: 'signature', value: `${data.signature}` }, { name: 'key', value: key }, ], method: http.RequestMethod.POST, header: { Accept: '*/*', // 设置header确保参数类型为FormData "Content-Type": "multipart/form-data" }, // 请求地址 url: data.host }); return new Promise((res, rej) => { result.on('progress', (u, t) => { console.log('进度', u / t); }); result.on('complete', e => { console.log('ok', JSON.stringify(e)); res(`${data.host + key}`); }); result.on('fail', e => { console.log('错误', JSON.stringify(e)); rej(e); }) }); } 五、注意事项与常见问题权限要求 需要在 config.json 中声明文件访问权限:{ "requestPermissions": [ { "name": "ohos.permission.READ_MEDIA", "reason": "需要访问媒体文件" } ] } 文件处理注意系统媒体库中的文件 URI 通常是临时的,建议复制到应用私有目录再使用const name = Date.now() + ""; const key = `${new Date().toLocaleDateString()}/${name}`; const context = getContext(); const destPath = `${context.cacheDir}/${name}`; const file = fileIo.openSync(uri); fileIo.copyFileSync(file.fd, destPath); console.log(destPath); 大文件处理时建议进行异步操作,避免 UI 卡顿request.uploadFile文件上传时候一直报错 401兼容性注意:不同鸿蒙版本的 API 可能存在差异,建议在开发前查阅对应版本的官方文档部分参数(如 title、initialDirectory)可能只在特定版本中支持通过合理配置 PhotoViewPicker 的各项参数,开发者可以轻松实现符合需求的媒体选择功能,为应用增色不少。
-
贡献 OpenHarmony 库关键配置 #自研框架#ArkUI-X#三方框架#OpenHarmony#HarmonyOS创建第三方库打开 DevEco Studio 创建一个项目,然后创建一个库模块,选择 static library。详细步骤可参考:开发静态共享包。在项目内会生成 library 文件夹对所创建的库模块进行开发,需要完善库模块中生成的 oh-package.json5 文件,有关三方库名称的规则,请参阅三方库名称指南章节,其他配置项的设置请参考 oh-package.json5 配置说明。目录分析 library // HAR根目录 ├─libs // 存放用户自定义引用的Native库,一般为.so文件f └─src │ └─main │ ├─cpp │ │ ├─types // 定义Native API对外暴露的接口 │ │ │ └─liblibrary │ │ │ ├─index.d.ts │ │ │ └─oh-package.json5 │ │ ├─CMakeLists.txt // CMake配置文件 │ │ └─napi_init.cpp // C++源码文件 │ └─ets // ArkTS源码目录 │ │ └─components │ │ └─MainPage.ets │ ├─resources // 资源目录,用于存放资源文件,如图片、多媒体、字符串等 │ └─module.json5 // 模块配置文件,包含当前HAR的配置信息 ├─build-profile.json5 // Hvigor编译构建所需的配置文件,包含编译选项 ├─hvigorfile.ts // Hvigor构建脚本文件,包含构建当前模块的插件、自定义任务等 ├─Index.ets // HAR的入口文件,一般作为出口定义HAR对外提供的函数、组件等 └─oh-package.json5 // HAR的描述文件,定义HAR的基本信息、依赖项等 library/build-profile.json5 建议开启代码混淆{ "apiType": "stageMode", "buildOption": {}, "buildOptionSet": [ { "name": "release", "arkOptions": { // 混淆相关参数 "obfuscation": { "ruleOptions": { // true表示进行混淆,false表示不进行混淆。5.0.3.600及以上版本默认为false "enable": true, // 混淆规则文件 "files": ["./obfuscation-rules.txt"] }, // consumerFiles中指定的混淆配置文件会在构建依赖这个library的工程或library时被应用 "consumerFiles": ["./consumer-rules.txt"] } } } ], "targets": [ { "name": "default" } ] } build-profile.json5 设置 useNormalizedOHMUrl{ "app": { "products": [ { "buildOption": { "strictMode": { "useNormalizedOHMUrl": false } } } ] } } 在库模块的根目录中,创建一个 README.md(不区分大小写) 文件,描述您三方库的代码以及如何使用它,文件不能为空。README.md 模板# feilongui FeilongUI 是一款功能丰富、高效易用的 UI 库,旨在帮助开发者快速构建美观、交互流畅的用户界面。以下是该库的详细使用指南。 ## 安装命令 README.md 中未包含安装命令会导致审核失败 `ohpm install feilongui` ## 模块介绍 README.md 中缺少简要的三方库使用说明会导致审核失败没有文件会报错ohpm ERROR: HttpCode 400 The OHPM package must contain a non-empty readme.md file. ohpm ERROR: Publish failed, detail: The "Publish" request to url "https://ohpm.openharmony.cn/ohpm/feilongui" has failed在库模块的根目录中,创建一个 CHANGELOG.md 文件,描述您三方库的代码不同版本的变更内容,文件不能为空。没有文件会报错ohpm ERROR: HttpCode 400 The OHPM package must contain a non-empty changelog.md file. ohpm ERROR: Publish failed, detail: The "Publish" request to url "https://ohpm.openharmony.cn/ohpm/feilongui" has failedChangelog.md模板(CHANGELOG.md中未包含当前版本,也就是oh-package.json5中version值的版本。 CHANGELOG.md由清晰的版本号和该版本的修改内容组成,修改内容不可为空,不然过不了审核)# Changelog ## 1.0.0 \*\* Unreleased Added - 新增 [按钮],用于 [点击]。 Changed - 优化 [模块 / 功能名称] 的性能,提升 [具体指标,如响应速度、加载时间等]。 Deprecated - 标记 [某接口 / 方法 / 功能] 为废弃,计划在 [具体版本] 中移除。 Removed - 移除不再维护的 [某功能 / 模块]。 Fixed - 修复 [问题描述] 导致的 [具体错误,如程序崩溃、数据错误等]。 Security - 修复 [名称] 提升系统安全性。在库模块的根目录中,创建一个 LICENSE(不区分大小写) 文件,指定该软件包可以在什么样的许可下被使用、修改和共享,文件不能为空。许可证模板(LICENSE文件中许可证条款内容和oh-package.json5文件中许可证名称不一致会审核失败) 许可证获取地址Copyright [此处填写年份,如2025] [此处填写在library/oh-package.json5中的name,否则审核失败] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.没有 LICENSE 文件会报错ohpm ERROR: HttpCode 400 The OHPM package must contain a non-empty license file. ohpm ERROR: Publish failed, detail: The "Publish" request to url "https://ohpm.openharmony.cn/ohpm/feilongui" has failed在进行 publish 发布前,请先确保在 OpenHarmony 三方库中心仓上已经创建了帐号,且利用工具 ssh-keygen 生成公、私钥文件ssh-keygen -m PEM -t RSA -b 4096 -f ~/.ssh_ohpm/mykey ohpm config set key_path ~/.ssh_ohpm/mykey不签名发布时会报错ohpm ERROR: Publish failed, detail: The "key_path" is empty - configure "key_path" in the .ohpmrc file.不设置密码发布时会报错ohpm ERROR: Private key without passphrase is not supported. ohpm ERROR: You must config a encrypted private key using a non-empty passphrase. ohpm ERROR: Publish failed, detail: Not supported private key. 不上传公钥(~/.ssh_ohpm/mykey.pub)会报错ohpm ERROR: HttpCode 400 The publicKey verify failed! ohpm ERROR: Publish failed, detail: The "Login" request to url "https://ohpm.openharmony.cn/ohpm/login" has failed设置 publish_id不设置 publish_id 会报错ohpm ERROR: Publish failed, detail: The "publish_id" is empty - configure "publish_id" in .ohpmrc file.publish_id 设置错误会报错ohpm ERROR: HttpCode 400 The publishId is invalid! ohpm ERROR: Publish failed, detail: The "Login" request to url "https://ohpm.openharmony.cn/ohpm/login" has failed发布后再发布不更新版本号会报错ohpm ERROR: HttpCode 400 The OHPM package version already exists. ohpm ERROR: Publish failed, detail: The "Publish" request to url "https://ohpm.openharmony.cn/ohpm/feilongui" has failed利用 DevEco Studio 对开发后的库模块打成 HAR 包。( 详情请见:构建 HAR)在工具中构建 library 库时需要选中 library 文件夹内的文件构建后所在目录为library/build/default/outputs/default/library-signed.har后续发布需要使用这个路径
-
DevEcoStudio 中使用模拟器时如何过滤日志 #鸿蒙核心技术#鸿蒙开发者工具##DevEcoStudio在 Hilog > Settings > Filter 设置Log message: A03d00/JSAPP当你看到不断更新的日志时,你会不会崩溃因为 No-filters 模式下模拟器会输出系统所有日志信息,这个模式在开发中并不使用,可用自定义模式找到模拟器日志,选择自定义在 Filter name 中输入custom (可以忽略这一步)Log level 中选择Info在 Log message 输入框中输入A03d00/JSAPP(打印内容)操作后在控制台查看就行了其他日志内容类型启动相关日志:记录模拟器启动过程中的各种信息,如加载系统镜像、初始化硬件设备、配置网络等操作的结果和状态。如果模拟器启动失败,这里会包含导致失败的具体原因,例如端口冲突、虚拟机管理程序未启用或配置错误、权限不足等。系统运行日志:包含模拟器系统运行时的各种事件和状态信息,如进程的启动和停止、系统服务的运行情况、内存和 CPU 的使用统计等。这些信息有助于开发者了解模拟器的整体运行状况,判断是否存在资源瓶颈或系统异常。应用相关日志:当在模拟器中运行应用程序时,会记录应用的启动、暂停、恢复、销毁等生命周期事件,以及应用中发生的各种错误、警告和调试信息。这对于开发者调试应用在模拟器上的运行问题非常重要,可以帮助定位应用中的代码错误、资源泄漏等问题。硬件模拟日志:模拟器需要模拟设备的硬件功能,如屏幕、摄像头、传感器等。相关日志会记录硬件模拟的操作和状态,例如屏幕分辨率的设置、摄像头的调用情况、传感器数据的模拟等。如果应用依赖于特定的硬件功能,这些日志可以帮助确定硬件模拟是否正常工作,以及应用与模拟硬件的交互是否正确。日志级别DEBUG:调试级别日志,通常包含详细的调试信息,用于开发者在开发过程中深入了解程序的执行流程和变量状态等。在正式发布版本中默认不被打印,只有在调试版本或打开调试开关的情况下才会打印。INFO:信息级别日志,用于记录程序运行中的重要信息,如系统启动、模块加载、关键操作的执行等,有助于开发者了解程序的整体运行情况。WARN:警告级别日志,提示可能存在的问题或潜在的风险,如资源使用接近上限、不推荐的操作被执行等,但并不一定表示程序出现了错误。ERROR:错误级别日志,用于记录程序中发生的错误情况,如代码异常、文件读取失败、网络连接中断等,这些错误可能会导致程序的部分功能无法正常运行。FATAL:严重错误级别日志,通常表示程序出现了严重的错误,导致整个系统或关键功能无法继续运行,如系统崩溃、内存耗尽等。
-
CodeArts IDE 鸿蒙版本功能是否完整?鸿蒙系统终端是否有命令教程?
-
大家好哦 ,三月份的干货合集来了,这次包含又redis,MySQL,HarmonyOS,Linux,Python,GoLang,Nginx,spring等多方面内容,希望可以帮到大家1.Redis Key的数量上限及优化策略分享【转】https://bbs.huaweicloud.com/forum/thread-02127178647758557099-1-1.html2.MySQL多列IN查询的实现【转】https://bbs.huaweicloud.com/forum/thread-0282178647688443077-1-1.html3.MySQL新增字段后Java实体未更新的潜在问题与解决方案【转】https://bbs.huaweicloud.com/forum/thread-0211178647622373117-1-1.html4.浅谈mysql的sql_mode可能会限制你的查询【转】https://bbs.huaweicloud.com/forum/thread-02127178647532278098-1-1.html5.MySQL使用SHOW PROCESSLIST的实现【转】https://bbs.huaweicloud.com/forum/thread-0238178647442172080-1-1.html6.HarmonyOS Next音乐播放器技术栈详解【转】https://bbs.huaweicloud.com/forum/thread-0213178647328545104-1-1.html7.Linux上设置Ollama服务配置(常用环境变量)【转】https://bbs.huaweicloud.com/forum/thread-0274178647201397098-1-1.html8.GORM中Model和Table的区别及使用【转】https://bbs.huaweicloud.com/forum/thread-0238178647121910079-1-1.html9. Python 的 ultralytics 库功能及安装方法【转】https://bbs.huaweicloud.com/forum/thread-0211178647036676116-1-1.html10.Python如何在Word中查找并替换文本【转】https://bbs.huaweicloud.com/forum/thread-0213178646924252103-1-1.html?fid=56811.GoLand 中设置默认项目文件夹的实现【转】https://bbs.huaweicloud.com/forum/thread-0210178646835711094-1-1.html12.Python Geopy库地理编码和地理距离计算案例展示【转】https://bbs.huaweicloud.com/forum/thread-0282178646750927076-1-1.html13.Java RMI技术详解与案例分析https://bbs.huaweicloud.com/forum/thread-0274178534386630091-1-1.html14.Volatile不保证原子性及解决方案https://bbs.huaweicloud.com/forum/thread-0274178534309336090-1-1.html15.Redis数据结构—跳跃表 skiplist 实现源码分析https://bbs.huaweicloud.com/forum/thread-0282178533434493072-1-1.html16.Java Executors类的9种创建线程池的方法及应用场景分析https://bbs.huaweicloud.com/forum/thread-0210178533186291086-1-1.html17.Nginx性能调优5招35式不可不知的策略实战https://bbs.huaweicloud.com/forum/thread-0213178533127218096-1-1.html18.Tomcat的配置文件中有哪些关键的配置项,它们分别有什么作用?https://bbs.huaweicloud.com/forum/thread-0210178533048188084-1-1.html19.深度长文解析SpringWebFlux响应式框架15个核心组件源码 https://bbs.huaweicloud.com/forum/thread-0282178532893901071-1-1.html20.对比传统数据库,TiDB 强在哪?谈谈 TiDB 的适应场景和产品能力https://bbs.huaweicloud.com/forum/thread-02127178532544750088-1-1.html
-
技术栈详解1. HarmonyOS Next开发环境该项目基于HarmonyOS Next开发框架构建,使用DevEco Studio作为集成开发环境。HarmonyOS Next是华为自主研发的分布式操作系统,专为全场景智能设备提供统一的操作系统解决方案。相比传统移动应用开发,HarmonyOS具有分布式能力、一次开发多端部署等显著优势。2. ArkTS声明式开发范式项目代码采用ArkTS语言开发,这是一种基于TypeScript的声明式UI开发语言,专为HarmonyOS定制。主要特点包括:基于组件的UI构建:通过@Component装饰器定义可复用UI组件声明式编程:使用类似HTML的结构直接描述UI界面状态管理:使用@State等装饰器管理组件状态生命周期钩子:提供aboutToAppear、aboutToDisappear等生命周期方法3. 多媒体处理技术应用核心功能基于鸿蒙媒体管理框架实现,主要使用了:1import media from '@ohos.multimedia.media';这个框架提供了强大的音频处理能力:AVPlayer音频播放器:创建和管理音频播放实例状态管理机制:通过事件监听处理不同播放状态播放控制API:提供play()、pause()、stop()等方法进度控制:支持seek()方法实现播放位置跳转4. 组件化架构设计项目采用清晰的组件化设计思路,主要分为:入口组件:Index.ets作为应用入口页面功能组件:MusicPlayer.ets封装所有音乐播放相关逻辑和UI资源管理:通过resources目录统一管理应用资源这种架构设计使代码结构清晰,功能模块化,便于维护和扩展。核心功能实现剖析1. 音频播放器初始化与状态管理1234567891011121314151617181920initAudioPlayer() { if (this.audioPlayer === null) { this.audioPlayer = media.createAVPlayer(); // 设置音频源 this.audioPlayer.url = 'resource://raw/beautiful_now.mp3'; // 设置状态回调 this.audioPlayer.on('stateChange', (state) => { // 状态监听处理逻辑 }); // 错误回调 this.audioPlayer.on('error', (err) => { console.error(`播放器错误: ${err.code}, ${err.message}`); }); // 准备播放器 this.audioPlayer.prepare(); }}这段代码展示了鸿蒙音频播放器的创建和初始化过程,通过事件驱动的方式监听播放器状态变化,实现播放流程控制。2. 播放控制实现应用提供了三种基本控制功能:开始播放:调用audioPlayer.play()方法并启动计时器暂停播放:调用audioPlayer.pause()方法并停止计时器停止播放:调用audioPlayer.stop()方法并重置播放状态这些控制方法配合状态监听,构成了完整的音频控制流程。3. 进度显示与交互123456789101112131415161718192021222324252627282930313233// 进度条和时间显示部分Row() { Text(this.formatTime(this.currentTime)) .fontSize(14) .width(50) Slider({ value: this.currentTime, min: 0, max: this.duration, step: 1, style: SliderStyle.OutSet }) .width('80%') .trackThickness(4) .showTips(true) .onChange((value: number) => { this.currentTime = value; }) .onTouch((event) => { if (event.type === TouchType.Down) { this.sliderMoving = true; } else if (event.type === TouchType.Up) { this.sliderMoving = false; this.setPosition(this.currentTime); } }) Text(this.formatTime(this.duration)) .fontSize(14) .width(50) .textAlign(TextAlign.End)}此部分代码实现了进度条和时间显示功能,特别值得注意的是:使用Slider组件提供直观的进度显示和控制通过onTouch事件实现拖动检测,确保用户体验流畅时间格式化显示,提升用户体验4. UI设计与用户体验应用界面设计简洁美观,主要包括:专辑封面区域:以蓝色背景块模拟专辑封面歌曲信息显示:包含歌曲名称和艺术家信息播放控制按钮:采用圆形设计,提供直观的播放/暂停/前进/后退功能进度控制区域:包含进度条和时间显示停止按钮:提供一键停止功能整体UI遵循了现代移动应用设计理念,布局合理,操作流畅。技术特点与优势1. 声明式编程范式与传统命令式编程相比,ArkTS声明式UI编程具有以下优势:代码更简洁:直接描述界面结构,减少样板代码易于理解:UI结构与实际渲染结果对应明确状态驱动:UI随状态自动更新,无需手动DOM操作2. 组件生命周期管理12345678910111213aboutToAppear() { // 组件出现时初始化播放器 this.initAudioPlayer();} aboutToDisappear() { // 组件消失时释放资源 this.stopTimer(); if (this.audioPlayer) { this.audioPlayer.release(); this.audioPlayer = null; }}这段代码展示了鸿蒙应用中组件生命周期管理的最佳实践,确保资源在适当时机被创建和释放,避免内存泄漏。3. 响应式状态管理通过@State装饰器,实现了组件状态的响应式管理:1234@State currentTime: number = 0; @State duration: number = 180; @State isPlaying: boolean = false; @State sliderMoving: boolean = false; 状态变化会自动触发UI更新,简化了状态同步逻辑。4. 事件处理机制应用中大量使用事件处理机制,如:播放器状态变化监听按钮点击事件处理滑块拖动事件处理这些事件处理逻辑清晰,使得用户交互更加流畅可靠。开发经验汇总1. 资源管理应用通过路径resource://raw/beautiful_now.mp3访问音频资源,体现了鸿蒙系统的资源管理机制。2. 错误处理代码中包含完善的错误处理机制,尤其是对播放器错误的监听和处理,提高了应用的稳定性。3. 性能优化使用计时器定期更新进度,而非频繁查询组件生命周期中及时释放资源拖动状态管理,避免拖动时的频繁更新
-
给Web开发者的HarmonyOS指南02-布局样式本系列教程适合鸿蒙 HarmonyOS 初学者,为那些熟悉用 HTML 与 CSS 语法的 Web 前端开发者准备的。本系列教程会将 HTML/CSS 代码片段替换为等价的 HarmonyOS/ArkUI 代码。开发环境准备DevEco Studio 5.0.3HarmonyOS Next API 15布局基础对比在Web开发中,我们使用CSS来控制元素的布局和样式。而在HarmonyOS的ArkUI中,我们使用声明式UI和链式API来实现相同的效果。本文将对比两种框架在布局方面的异同。盒子模型在Web开发中,CSS盒子模型包含内容(content)、内边距(padding)、边框(border)和外边距(margin)。在ArkUI中,这些概念依然存在,只是写法有所不同,容易上手。HTML/CSS代码:<div class="box"> 盒子模型 </div> <style> .box { box-sizing: border-box; /* 内容 */ width: 150px; height: 100px; /* 内边距 */ padding: 10px; /* 边框 */ border: 10px solid pink; /* 底部外边距 */ margin-bottom: 10px; } </style> ArkUI代码:Text('盒子模型') .width(150) .height(100) .padding(10) .border({ width: 10, style: BorderStyle.Solid, color: Color.Pink }) .margin({ bottom: 10 }) 背景色和文字颜色在Web开发中,我们使用 background-color 和 color 属性来设置背景色和文字颜色。在ArkUI中,我们使用 backgroundColor 和 fontColor 方法。HTML/CSS代码:<div class="box"> 背景色、文字色 </div> <style> .box { /* 背景色 */ background-color: #36d; /* 文字色 */ color: #fff; } </style> ArkUI代码:Text('背景色、文字色') .backgroundColor('#36d') .fontColor('#fff') 内容居中在Web开发中,我们使用 display: flex 配合 justify-content 和 align-items 实现内容居中。在ArkUI中,我们可以使用 Column 或 Row 组件配合 justifyContent 和 alignItems 属性。HTML/CSS代码:<div class="box"> 内容居中 </div> <style> .box { display: flex; justify-content: center; align-items: center; } </style> ArkUI代码:Column() { Text('内容居中') } .backgroundColor('#36D') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .width(150) .height(100) .padding(10) 圆角在Web开发中,我们使用border-radius属性来设置圆角。在ArkUI中,我们使用borderRadius方法。HTML/CSS代码:<div class="box"> 圆角 </div> <style> .box { border-radius: 10px; } </style> ArkUI代码:Text('圆角') .width(150) .height(100) .backgroundColor('#36D') .borderRadius(10) 阴影效果在Web开发中,我们使用box-shadow属性来设置阴影效果。在ArkUI中,我们使用shadow方法。HTML/CSS代码:<div class="box"> 阴影 </div> <style> .box { box-shadow: 0 6px 50px rgba(0, 0, 0, 0.5); } </style> ArkUI代码:Text('阴影') .width(150) .height(100) .backgroundColor('#F5F5F5') .shadow({ offsetX: 0, offsetY: 6, radius: 50, color: 'rgba(0, 0, 0, 0.5)', }) 布局容器和轴向基本容器在Web开发中,我们使用<div>作为通用容器。在ArkUI中,我们主要使用Column和Row组件,注意 alignItems 需区分轴向。HTML/CSS代码:<div class="column"> <!-- 垂直方向布局 --> </div> <div class="row"> <!-- 水平方向布局 --> </div> <style> .column { display: flex; flex-direction: column; align-items: center; } .row { display: flex; flex-direction: row; align-items: center; } </style> ArkUI代码:Column() { // 垂直方向布局,交叉轴水平居中 } .alignItems(HorizontalAlign.Center) Row() { // 水平方向布局,交叉轴垂直居中 } .alignItems(VerticalAlign.Center) 关键区别总结样式应用方式:HTML/CSS:使用选择器和属性声明样式ArkUI:使用链式API直接在组件上设置样式布局容器:HTML:使用 <div> 等标签,配合CSS实现布局ArkUI:使用专门的布局组件如 Column、Row 等组件,配合样式属性布局单位使用:HTML/CSS:使用 px、em、rem、百分比等单位ArkUI:使用 px、vp、lpx 、百分比等单位,使用数字单位 vp 可省略样式继承:HTML/CSS:通过CSS选择器实现样式继承ArkUI:没有样式继承学习建议理解链式API:熟悉ArkUI的链式API调用方式掌握常用样式方法的命名规则布局思维转变:从CSS盒模型思维转向组件化思维理解ArkUI的布局容器特性样式设置习惯:养成使用链式API设置样式的习惯注意样式方法的参数格式组件嵌套:合理使用组件嵌套实现复杂布局注意组件的父子关系总结作为Web开发者,迁移到 HarmonyOS 开发需要适应新的布局和样式设置方式。概念其实非常相似,通过理解这些差异,并掌握ArkUI的组件化开发方式,Web开发者可以快速上手HarmonyOS开发。希望这篇 HarmonyOS 教程对你有所帮助,期待您的 👍点赞、💬评论、🌟收藏 支持。
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签