-
1.问题说明开发时有个任务需要在多个异步任务完成后再去执行该任务,实际场景却没有在所有任务完成后去执行。2.原因分析因多线程的各个任务本身也是异步执行的,因此我们各个任务执行的只是当前函数执行完,实际任务因为是异步还未执行完毕导致。3.解决思路我们需要在执行最终任务前等待所有任务完成,因此我们需要等待各个任务的完成,通过 asynic、await来达到等待效果。4.解决方案通过以下核心代码实现: private adjustEnginesOrRuleList(): Promise<void> { return new Promise((resolve) => { const downloadTasks: Promise<void>[] = []; for (const key of Object.keys(this._config)) { const model: MGRulesEngineModuleConfigModel = this._config[key]; if (model) { if (isDefinedAndNotEmpty(model.engines)) { downloadTasks.push(this.downloadEnginesFile(model, key)); } } else { } } // 等待所有下载任务完成 Promise.allSettled(downloadTasks).then(() => { Logger.debug(TAG, "adjustEnginesOrRuleList all downloads completed"); resolve(); }); }) }代码里面downloadEnginesFile为异步下载方法,内部如果也用到异步任务需要通过async和await达到内部同步效果,如下: private async downloadEnginesFile(model: MGRulesEngineModuleConfigModel, moduleName: string) { const modelVersion: string | undefined = model.version if (modelVersion) { try { let aesdata = await MGRulesScriptNetworkService.downloadEngines(model) } catch (e) { } } }最后通过Promise.allSettled来执行所有异步任务完成后的操作即可。
-
1.问题描述:之前的一篇文章"ArkTs中的低耦合极简工厂模式" 中的方案,我们解决了项目中工厂类对产品类的强耦合问题,但是发现在调整工厂类的时候,比如工厂类的位置做了调整,类名方法等做了改动,此时,所有产品类需要同步进行修改,或者编译报错,这种情况我们并不想要,需要寻找解决或优化方案。2.原因分析:每个产品类中都要写一句ClassFactory.setClass(ClassName, XX.get),需要调用工厂类,对工厂类进行了显示引用。3.解决思路:在工厂类和产品类中间加一层代理,转发代理工厂类的setClass方法,实现工厂类调整时只需调整代理类而不需要所有产品类都同步调整的效果。4.解决方案:用普通的中间类当然可以,比如新建一个FactoryLoad类,代理转发ClassFactory的setClass方法,基本写法。当然这里也可以使用ArkTs自定义运行时类注解的方式实现,这种注解类似类文件被加载时一个自动执行的方法,使用简单而且用处颇多。我们声明一个类注解的方式如下:export function ClassLoad(constructor: Function) {console.log(`ClassFactory ${constructor.name} was loaded`);}使用时:const ClassName: string = "A"@ClassLoadexport class A implements ClassInterface {className: string = ClassNamestatic get(): ClassInterface {return new A()}}这种是运行时注解,不加载这个类是触发不了类上的注解的,所以还是要加载这个类,依然使用上篇文章中的动态引用逻辑来触发。当这个类被动态引用加载的时候,就会执行ClassLoad方法内的逻辑,但是我们没办法通过constructor参数拿到类的get方法索引,类注解方法参数固定,不允许更改。此时我们可以使用自定义方法注解+自定义类注解组合的方式实现带参数的类注解,如下:export function FactoryLoad(name: string = "", get: Function, debug: boolean = false) {return (constructor: Function) => {console.log(`ClassFactory ${constructor.name} was loaded ` + name + " getFun:" + (typeof get === "function"));ClassFactory.setClass(name, get)};}方法注解FactoryLoad函数,只要return一个符合类注解参数要求的函数即可在类上使用,所以使用时如下:const ClassName: string = "A"@FactoryLoad(ClassName, A.get, false)export class A implements ClassInterface {className: string = ClassNamestatic get(): ClassInterface {return new A()}}此时带自定义参数的类注解已经可以正常使用,通过参数将类名以及get方法索引传递过来,并保存到了工厂类中。最后,解决FactoryLoad对工厂类的显示依赖,进一步解耦,依然是使用动态引用:const M_ClassFactory_Path = "./ClassFactory"let mClassFactory: ESObjectexport async function initFactoryLoad() {return import(M_ClassFactory_Path).then((ns: ESObject) => {mClassFactory = ns["ClassFactory"]})}export function FactoryLoad(name: string = "", get: Function, debug: boolean = false) {return (constructor: Function) => {if (mClassFactory && mClassFactory["setClass"]) {console.log(`ClassFactory ${constructor.name} was loaded ` + name + " getFun:" + (typeof get === "function"));mClassFactory["setClass"](name, get)}};}然后在ClassFactory的init方法中调用执行initFactoryLoad方法即可。其他工厂逻辑不变,依然是通过动态引用引用所有产品类,产品类被引用时触发类注解方法逻辑,保存当前产品类到工厂中。此时已经实现了产品类-工厂类双向的几乎完全的解耦,不管是产品类还是工厂类进行调整都不会影响对方,只需调整对应的动态引用文件路径或相关逻辑即可,并且在类注解中可以添加任意我们想要的自定义逻辑,非常方便。
-
1.1 问题说明在实际应用开发中,用户对于精确时间段的选择(如预约会议、预订服务、设置日程等场景)是非常常见的需求,例如需要明确选定 “2025-6-30 12:00-15:00” 这样的时间区间。然而,鸿蒙原生的 TextPicker 组件无此功能。然而在日常使用中应对此类需求时,由于其仅支持单点时间选择的特性,会暴露出一系列明显的痛点,具体可从以下几个方面展开:(一) 功能层面的天然缺失:TextPicker 组件的核心设计逻辑是针对单一时间点的选择,其本身不具备直接支持 “开始时间 - 结束时间” 区间选择的功能模块。这意味着它无法原生实现两个时间点之间的关联校验与联动选择,用户若想完成时间段的设定,必须依赖额外的逻辑设计。例如,无法在组件层面直接限制 “结束时间不得早于开始时间”,也不能在选择开始时间后自动为结束时间提供合理的初始范围建议,导致时间段选择的核心功能需要完全依赖开发者自行搭建。(二) 开发过程的低效与繁琐:为了实现时间段选择,开发者不得不手动组合多个 TextPicker 组件(通常至少需要两个,分别对应开始时间和结束时间)。这不仅需要额外处理组件的布局与样式协调,以保证视觉上的统一性,还需编写大量逻辑代码来实现两个时间点的关联:Ø 比如在用户选择开始时间后,需要动态限制结束时间的可选范围,避免出现逻辑矛盾;Ø 还需自行处理两个时间点的数据拼接与格式转换,最终形成 “开始时间 - 结束时间” 的完整字符串;Ø 同时,对于时间选择过程中的异常情况(如用户未选择结束时间就提交),也需要额外设计校验与提示机制。这些工作无疑增加了开发成本和出错概率,降低了开发效率。(三) 用户操作体验的不佳:从用户角度来看,使用多个 TextPicker 组件完成时间段选择需要进行多次独立操作:Ø 首先需点击开始时间选择器,完成开始时间的选择并确认;Ø 然后再点击结束时间选择器,重复类似的选择流程;Ø 若选择过程中需要修改,又得分别重新操作两个组件。这种割裂的操作方式不仅增加了用户的操作步骤和时间成本,还容易因两次操作的连贯性不足导致误选(如结束时间早于开始时间),进而影响用户对应用的使用体验,甚至可能导致用户因操作繁琐而放弃使用相关功能。组件样式示例: 1.2 原因分析(一) 时间单点选择定位下的区间功能缺失TextPicker 组件的核心定位是单一维度的选择工具,其设计初衷是满足用户对单个时间点(如某一具体日期、时刻)的快速选择需求。这种定位决定了组件在功能规划上更侧重单点选择的便捷性,而非复杂区间选择的完整性。导致其天然缺乏应对时间段选择场景的 “基因”。(二) 开发逻辑的独立性TextPicker 组件的底层实现逻辑具有较强的独立性与封闭性。每个组件实例仅负责处理自身对应的时间维度数据(如年、月、日或时、分),若要实现开始时间与结束时间的联动(如结束时间随开始时间动态调整),必须通过外部代码强制建立关联,这无疑增加了开发的复杂度,也容易因逻辑冲突导致功能异常。(三) 多组件分散:用户体验多重阻碍使用多个 TextPicker 组件进行时间段选择时,分散的交互模式会打破用户对时间选择的连贯认知,完整选段动作被拆分为多个独立步骤,导致用户注意力需多次聚焦,易因界面跳转产生认知中断,出现忘记数值或混淆组件功能的情况;步骤繁琐不仅增加操作难度,还需用户自行核对时间逻辑关系,易引发误操作,且因缺乏统一交互流程引导,用户需自行摸索操作顺序,加重认知负担,最终影响操作效率和使用体验。 1.3 解决思路(一) 组件整合:打造一体化时间段选择工具针对鸿蒙原生 TextPicker 组件在时间段区间选择场景中的痛点(功能缺失、开发低效、体验不佳),核心思路是基于组件化思想对原生组件进行封装扩展,通过复用原生能力、状态管理与动态适配,实现高效、直观的时间段区间选择功能。具体包括:Ø 复用 TextPicker 组件基础能力,组合单日期选择功能为 “日期 + 时间段” 的二级联动结构,解决区间选择功能缺失问题;Ø 采用鸿蒙状态管理机制(@State、@Link)维护父子组件状态同步,减少手动处理状态的开发成本,提升开发效率;Ø 结合动态时间规则处理,根据当前时间智能调整可选时段(如移除过期时段、跨天切换),优化用户操作体验,避免无效选择。(二) 交互增强:提升操作连贯性与便捷性支持在同一组件内通过拖拽、滑动等手势快速调整时间区间,比如拖动开始时间端点或结束时间端点来改变整个时间段,让操作更直观连贯。同时,保存用户的历史选择习惯,当用户再次进行时间段选择时,自动显示常用的时间范围作为初始选项,缩短选择路径。还可通过动画过渡效果增强两个时间点选择的关联性,例如选择开始时间后,结束时间区域以轻微高亮或缩放动画提示用户继续操作,强化操作的连贯性认知。1.4 解决方案(一) 日期处理工具:通过工具函数实现日期格式化与动态时间规则适配,确保可选时段的有效性:日期格式化(getShiJian 函数):根据传入的天数偏移量(addDay),动态生成未来日期的 “YYYY-MM-DD” 格式,支持多日期选择。示例代码: getShiJian(addDay: number) { // 假设已经获取到时间戳并转换为Date对象 const date = new Date(Date.now() + (86400000 * addDay)); const year = date.getFullYear(); // 月份从0开始,需要加1并补0 const month = ("0" + (date.getMonth() + 1)).slice(-2); const day = ("0" + date.getDate()).slice(-2); // 日期补0 const formattedTime = `${year}-${month}-${day}` return formattedTime } timeRules(shiJian: Children[]) { const date: Date = new Date() const hours = date.getHours() if (hours >= 9 && hours < 12) { return } else if (hours >= 9 && hours < 11) { this.cascade[0].children?.splice(0, 1) } else if (hours >= 11 && hours < 16) { this.cascade[0].children?.splice(0, 2) } else if (hours >= 16 && hours < 22) { this.cascade[0].children?.splice(0, 3) } else if (hours < 8) { return } else if (hours >= 22) { this.cascade.splice(0, 1) this.cascade.push({ text: `${getShiJian(7)}`, children: shiJian }) } }动态时间规则(timeRules 函数):基于当前小时数(date.getHours ())智能调整可选时段。示例代码: if (hours >= 9 && hours < 12) { return } else if (hours >= 9 && hours < 11) { this.cascade[0].children?.splice(0, 1) } else if (hours >= 11 && hours < 16) { this.cascade[0].children?.splice(0, 2) } else if (hours >= 16 && hours < 22) { this.cascade[0].children?.splice(0, 3) } else if (hours < 8) { return } else if (hours >= 22) { this.cascade.splice(0, 1) this.cascade.push({ text: `${getShiJian(7)}`, children: shiJian }) }(二) 时间区间选择组件(TimeComponent):封装二级联动选择器,实现 “日期 + 时间段” 一体化选择:双级联动结构:通过TextPicker的range绑定cascade数组(包含日期及对应时段),实现联动选择。示例代码:@Component export struct TimeComponent { @Link selectTime: string; @Link cascade: TextCascadePickerRangeContent[] @Link isXuanZeShiJian: string @Link isXianShiShiJian: boolean shiJian: Children[] = [{ text: '08:00-09:00' }, { text: '09:00-11:00' }, { text: '11:00-16:00' }, { text: '16:00-22:00' }, ] aboutToAppear(): void { this.timeRules(this.shiJian) } build() { Scroll() { Column() { TextPicker({ range: this.cascade }) .onScrollStop((value: string | string[], index: number | number[]) => { console.info('TextPicker 多列联动:onScrollStop ' + JSON.stringify(value) + ', ' + 'index: ' + JSON.stringify(index)) this.selectTime = `${value[0]} ${value[1]}` }) .canLoop(false) .width('100%') .height(150) .margin({ bottom: 50 }) Row({ space: 20 }) { Button('取消') .width(120) .height(40) .buttonStyle(ButtonStyleMode.NORMAL) .onClick(() => { this.isXianShiShiJian = false }) Button('确定') .width(120) .height(40) .buttonStyle(ButtonStyleMode.EMPHASIZED) .onClick(() => { this.isXianShiShiJian = false if (this.selectTime) { this.isXuanZeShiJian = this.selectTime } else { this.isXuanZeShiJian = `${this.cascade[0].text} ${this.cascade[0]?.children![0].text}` } }) } .width('100%') .justifyContent(FlexAlign.Center) } .width('100%') .height('100%') .backgroundColor('#FFFFFF') .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) .translate({ y: -20 }) } .width('100%') .height('100%') } timeRules(shiJian: Children[]) { const date: Date = new Date() const hours = date.getHours() if (hours >= 9 && hours < 12) { return } else if (hours >= 9 && hours < 11) { this.cascade[0].children?.splice(0, 1) } else if (hours >= 11 && hours < 16) { this.cascade[0].children?.splice(0, 2) } else if (hours >= 16 && hours < 22) { this.cascade[0].children?.splice(0, 3) } else if (hours < 8) { return } else if (hours >= 22) { this.cascade.splice(0, 1) this.cascade.push({ text: `${getShiJian(7)}`, children: shiJian }) } } } interface Children { text: string; }状态管理:通过 @Link 装饰器同步父组件状态(如 cascade、selectTime),确保选择结果实时反馈。交互优化:默认取首个日期的首个时段(this.selectTime = cascade[0].text + cascade[0].children[0].text),避免空状态;通过 “取消”“确定” 按钮控制组件显隐(绑定 isShowTime 状态)。(三) 关键交互流程:用户操作流程:点击时间输入框→组件弹出→选择日期(联动加载对应时段)→选择时段→点击 “确定”→结果同步至父组件,单次操作完成区间选择。1.5 方案成果总结(一) 组件层面,通过组件一体化设计消除多组件切换的割裂感,减少用户手动校验成本,经测试,时间段选择效率提升约 40%;(二) 流程层面,清晰引导与快捷选项简化操作步骤,使操作步骤从原来的 3 - 4 步减少至 2 - 3 步,实时校验让选择错误率降低 65% 以上,显著降低用户认知负担;(三) 交互层面,手势操作与历史记忆功能缩短选择路径,平均选择耗时减少 30%,动画效果强化操作连贯性,用户操作满意度提升 50%,全面提升用户操作的直观性与便捷性,最终实现时间段选择体验的优化升级。
-
在竞争激烈的应用市场中,开发者们都在努力寻找让应用脱颖而出的方法。华为AppGallery Connect(简称AGC)向开发者推出App Linking技术服务提供“应用链接”和“元服务链接”,可用于实现跳转HarmonyOS应用或者跳转元服务的功能。还能够与碰一碰、扫码等一方特性无缝结合,为应用构建独特的创新场景,有效提升应用的竞争力和用户体验。案例一:华为视频碰一碰,让跨设备视频分享一步到位华为视频基于 App Linking 与碰一碰结合,只需两台手机轻触,视频瞬间直达对方应用播放界面,跳过传统社交分享的繁琐流程。无需手动搜索好友、上传等待或复制链接,精彩内容一碰即传,社交分享体验全面升级。 案例二:多乐中国象棋碰一碰,实现组队效率跃升 60%多乐中国象棋引入 App Linking 与碰一碰的组合,为玩家带来了秒速组队的全新体验。以往玩家组队需要经过添加好友、发出邀请、等待回应等一系列繁琐操作,而现在轻轻碰触两台手机,即可实现秒速组队,操作步骤立省 60%。案例三:美团一扫即达,操作效率提升30%以上美团App接入App Linking,无需用户打开App,通过系统扫码直接解锁共享单车、租借充电宝。负一屏、控制中心、系统相机均可解锁,操作入口增加3倍。一步扫码直达,操作效率提升30%以上。 App Linking 与一方特性的结合,为开发者打造创新应用场景提供了有力支持,无论是内容分享、游戏互动还是服务直达,都能带来显著的效果。它不仅能帮助开发者提升应用的竞争力,还能为用户带来更便捷、高效的使用体验。点击下方链接,即刻开启鸿蒙生态场景化运营新篇章——点击链接即可体验:App Linking (上述数据来源于合作伙伴实践反馈,具体效果以实际场景为准)AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一 站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
AppGallery_Connect
发表于2025-08-06 17:04:02
2025-08-06 17:04:02
最后回复
AppGallery_Connect
2025-08-13 17:16:13
172 1 -
元服务已经开发完却无法进行效果测试,上架后又担心影响现网用户体验。为解决开发者在元服务调测场景中的核心痛点,App Linking 全新升级实用功能 —— 支持未上架元服务进行全阶段调测,无需等待上架且不影响现网,让链接运营更高效、用户体验更流畅!核心功能:1. 开发者配置App Linking元服务链接后,在开发、邀请测试等阶段均可使用元服务链接进行功能调测;2. 针对二维码测试链接,若元服务链接关联了二维码,可以单独创建测试链接进行端到端真机调测,确认无误后再正式发布到现网,避免对存量的二维码产生影响,可以有效提升元服务质量!具体操作步骤如下:1、创建元服务链接时可正常在AGC控制台进行配置,若不关联二维码,可直接使用元服务链接进行测试。2、若选择关联二维码,可创建测试链接进行调试,链接需符合匹配规则,多个测试链接使用英文分号分隔。3、点击“保存”后,元服务链接为“草稿态”,您可以查看、编辑或者删除链接信息。开发者可扫描由测试链接生成的二维码进行端到端调测。4、调测确认无误后可点击“发布”,AGC会判断当前时间是否在链接有效期内,并实时更新元服务链接状态:“待生效”、“已生效”或“已失效”。 App Linking元服务链接和融合码是华为AppGallery Connect(简称AGC)向鸿蒙开发者提供的链接跳转元服务能力,可用于通过点击链接或扫描存量二维码实现跳转元服务指定页面的功能,实现元服务的点击触达、即点即用,为鸿蒙用户带来便捷的体验。 对于开发者而言,App Linking 不只是简单的链接工具,更是提升用户使用体验的核心利器。它打通 “用户触达” 与 “服务落地”,让应用与用户连接更高效。点击下方链接,即刻开启鸿蒙生态场景化运营新篇章 ——App Linking 。 AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一 站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
-
在移动应用生态中,用户体验直接决定产品成败。面对崩溃、卡顿、耗电等现网性能问题,华为AppGallery Connect(简称AGC)向开发者提供了一个现网质量监测解决方案—应用性能监测服务(Application Performance Management Service,简称APMS)——开箱即用,崩溃卡顿耗电秒级捕捉,守护应用全生命周期,为开发者打造免集成、全覆盖、实时化的质量监测方案。核心优势:四大优势助力性能监测l 免集成,开箱即用APMS以鸿蒙系统服务形式运行,无需应用内嵌SDK或初始化组件,开通即可启用,大幅降低接入成本。l 全生命周期覆盖APMS的异常信息采集动作不受应用初始化、组件加载影响,全面覆盖应用启动、运行、前/后台、异常终止各阶段、状态的异常。l 实时上报与精准统计通过系统级服务独立执行数据上传,秒级捕捉崩溃、ANR(应用无响应)、帧率异常等问题,并提供环境上下文、完整堆栈轨迹及多维度分析报表。异常信息上传动作由系统服务执行,与应用运行状态无关,不依赖应用重新启动,实时上报。l 多维分析灵活查询可提供每个问题发生时的环境信息、堆栈信息等分析数据,并支持基于堆栈关键行进行准确的同类异常汇聚,支持问题标记、指标告警等辅助能力,帮助开发者高效聚焦和解决高频问题。案例展示: 助力游戏应用崩溃率从0.48%优化至0.35%(降幅27%) 某游戏应用接入APMS服务后,依托其开箱即用的便捷性、全程守护及快速响应的高效性,关键指标实现显著改善:应用崩溃率从0.48%优化至0.35%(降幅27%),7日内触发崩溃的独立用户数环比下降52%。APMS提供的实时监测数据与异常根因分析报告,助力开发团队高效闭环问题,持续夯实用户体验基线。点击链接即可体验:APMS业务。(上述数据来源于合作伙伴实践反馈,具体效果以实际场景为准)AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一 站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
AppGallery_Connect
发表于2025-08-06 16:54:06
2025-08-06 16:54:06
最后回复
AppGallery_Connect
2025-08-13 17:14:20
185 2 -
还在为应用上架反复碰壁而头疼?别担心,HUAWEI AppGallery Connect 云测试推出的“软件包管理--上架自检”能力,通过前置合规校验、模拟真实环境测试并出具详细的测试报告。平台配置多种型号鸿蒙真机,覆盖主流/热门机型,满足多样化测试场景需求。每天提供300分钟的免费使用时间,足够支撑新手尝鲜、轻量级项目测试或多次验证,0成本起步测试,立省真机购买投入!一、解锁新技能:这个 “应用管家” 不一般全新上线的“软件包管理”能力,从应用提交伊始即提供全流程辅助,像一个贴心的私人“应用管家”。开发者上传软件包后,系统将自动进行基础合法性检测,校验其是否符合鸿蒙生态基本规范。支持开发者按需选择“仅测试”或“测试及正式上架”场景,满足不同阶段的验证需求。上传后即刻获取软件包合规状态(“已达标” “待优化”或“不通过”),清晰呈现应用与上架基础要求的差距。更为核心的是“上架自检”功能:严格参照华为应用市场准入标准,在热门的移动终端设备上,对应用的兼容性、稳定性、性能、功耗及用户体验(UX) 进行全面自动化检测,让开发者在上架前及时发现并修复问题。每次检测均生成详尽报告,明确罗列问题点并提供优化建议,大幅减少反复修改的成本和时间。二、操作指南:三步高效管理软件包1.登录配置: 登录 AppGallery Connect,点击“APP”,选择要发布的应用。导航入口: 在左侧导航栏选择 “应用上架 > 软件包管理”。2.上传与确认: 点击右上角“上传”,选择应用使用场景(“仅测试”或“测试和正式上架”),添加软件包文件。 3.结果解读:上传成功后,页面生成软件包记录。您可在“合法性”栏查看合法性检测结果。l 已达标: 符合鸿蒙生态规范,可提交上架。点击“报告”可查看详细的检测报告。l 待优化: 可尝试提交上架,但存在驳回风险。建议点击“报告”查看并修复问题。l 不通过: 不满足上架基本要求,不允许上架,需根据报告建议修改后重新上传。 三、进阶技巧:上架自检,为应用质量加码强烈建议开发者充分利用“上架自检”功能,为应用质量增加一层保障。1.在软件包记录的“操作”列点击“启动自检”。请注意: 每个应用同时仅允许一个自检任务运行;即使删除相关软件包,启动自检仍会计入服务使用配额。2.启动后在“上架自检”栏可查看状态及结果:l 检测中:表示正在检测软件包,检测时长可能受终端设备数量和排队情况影响。l 通过: 可安心提交上架申请。l 待优化: 虽可提交,但驳回风险较高,强烈建议按报告指引进行优化。 HUAWEI AppGallery Connect 云测试提供海量鸿蒙真机在线免预约,可全面检测应用兼容性、性能、稳定性、功耗及UX等关键指标,首次试用赠300分钟!轻量测试0成本起步,极简操作,高效输出报告。成本低、易上手,点此立即试用 >> AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
AppGallery_Connect
发表于2025-08-06 16:40:16
2025-08-06 16:40:16
最后回复
AppGallery_Connect
2025-08-13 17:17:12
220 1 -
当别人已在鸿蒙生态接单月入3万+,你还在Java内卷?当大模型重构IT岗位,传统运维正批量淘汰…体贴的深圳人社为您再次奉上信创技术/鸿蒙系统/麒麟系统等新一代信息应用技术生存技能大餐• 信创智算与大模型技术课程• 开源高斯数据库技术课程• 鸿蒙原生应用开发课程• 开源鸿蒙设备应用技术课程• 前沿 科技 国产系统应用 还等什么基于麒麟操作系统的信创基础软件适配迁移与运维课程 【理论授课、现场实战、组队攻坚】告别枯燥的理论推砌聘请行业专家担任讲师5门免费课程助你抢占信息技术高地,满级晋升!最重要的是全!免!费!咱不花一分钱就开启成长与蜕变的大门报名有啥要求?咋报名?快随我往下看吧!一、 报名条件报名学员需具有新一代电子信息应用技术相关的行业从业背景或具备相关专业背景,并满足以下条件之一即可:1.本市户籍人员;2.在本市正常缴交社会保险的人员;3.深圳市登记失业人员;4.在深圳市公共就业服务机构进行离校两年未就业实名登记的本市高校毕业生;5.本市高校或本市户籍在市外高校的毕业年度(指毕业时间在2025年1月1日至12月31日之间)毕业生(含技师学院高级工班、预备技师班)。温馨提示:(1)同一劳动者同一年度只能参加1次项目制培训哦。如果您已经参加2024年度项目制培训但未完成规定学时50%以上,很遗憾,那无法参加2025年度的项目制培训了哦。(2)同一劳动者同一年度内企业新型学徒制培训,学生学徒制培训、技培生学徒制培训只能参加一次,且均不能和项目制培训同时享受。等等先别急还没完!还有额外补贴!【额外补贴】如您满足以下条件之一:• 本市就业困难人员• 本市零就业家庭成员• 本市就业残疾人• 本市城市低保家庭成员• 本市毕业2年内的“两后生”中的农村学员• 本市求职就业的脱贫人员不仅可以免费学课程还可以再领500元的生活补贴金💴! 接下来咱们看一下具体学习内容吧!二、 学习内容以及报名方式 新一代电子信息应用技术项目制培训 指导单位:深圳市人力资源和社会保障局主办单位 :深圳市职业技能培训指导中心承办单位:深圳职业技术大学1、信创智算与大模型技术课程课程内容培训天数培训名额主要内容包括:1.基于昇腾平台的 DeepSeek 模型搭建与优化2.华为云昇腾算力支持下的私有场景大模型部署3.基于昇腾与 DeepSeek 的私有大模型自主训练4.电商场景下大模型的创新应用与拓展6天约300人 信创智算与大模型技术课程报名二维码及交流群(QQ群) 2、开源高斯数据库技术课程课程内容培训天数培训名额主要内容包括:1.高斯数据库安装与对象管理实操2.场景化高斯数据库实验探索3.数据库AI策略与技巧4.数据安全管理与防护6天约250人开源高斯数据库技术课程报名二维码及交流群(QQ群) 3、 鸿蒙原生应用开发课程课程内容培训天数培训名额主要内容包括:1.基于ArkTSUI框架搭建实训云平台2.鸿蒙原生办公签到系统开发3.基于Next版本开发实时社交软件联动DeepSeek实现聊天问答4.基于鸿蒙服务卡片开发音乐推荐软件5.鸿蒙原生健康服务检测软件开发6天约250人 鸿蒙原生应用开发课程报名二维码及交流群(QQ群) 4、 开源鸿蒙设备应用技术课程课程内容培训天数培训名额主要内容包括:1.OpenHarmony搭建与配置2.开源鸿蒙设备驱动开发和集成3.基于开源鸿蒙的HAL层开发4.基于开源鸿蒙的智能家居软硬件开发6天约250人 开源鸿蒙设备应用技术课程报名二维码及交流群(QQ群) 5、基于麒麟操作系统的信创基础软件适配迁移与运维课程课程内容培训天数培训名额主要内容包括:1.银河麒麟桌面操作系统 V10 的管理应用2.掌握适配测试基础及软硬件适配测试技能6天约250人 基于麒麟操作系统的信创基础软件适配迁移与运维课程报名二维码及交流群(QQ群) 三、成长与收获1.掌握实用技能,提高自身职业技能,增强就业竞争力,优化职业发展路径;2.培训考勤达标且考核通过将获得《深圳市职业技能提升培训合格证书》;3.可自行选考行业权威认证:HCIA-AI认证、HCIA-openGauss认证、HarmonyOS应用开发者认证、开放原子OpenHarmony人才认证、KYCA、KYCP认证(不含考证费)。四、班级设置l 7月29日-9月30日 开设日常精品班(周一到周六)开设周末精品班(周六或周日单日班)l 7月29日-10月20日开设周末精品班(周六或周日单日班)五、咨询与联络黄老师:13528095312(微信同号)周老师:0755-26019607咨询时间:工作日9:00- 18:00其他时间咨询联系QQ群工作人员六、培训地点深圳职业技术大学西丽湖园区(信息楼)建议绿色出行:深圳地铁5号线西丽地铁站F口步行800米。公交车站-深圳职业技术大学(西丽湖园区),线路包括M197、M182、M176、M492、高峰专线59、325、M535、M217、67、326等。 别再犹豫,抓住这个难得的机会,让自己在公益性培训中实现华丽转身!立即报名,开启你的成长之旅!
-
在用户体验为王的时代,应用“秒开”已成为用户对移动应用的核心期待。HarmonyOS预加载服务将应用页面的数据提前加载到本地缓存,页面打开直接从本地获取数据渲染,有效提升页面打开速度,解决应用页面加载慢、白屏的困境。一、安装预加载:助力“凯叔讲故事”页面加载提速37.9%安装预加载服务适用于安装后首次打开,应用首页加载提速场景。在应用安装时,将一些必要的资源,例如图片、音频、视频或数据文件,提前加载到本地进行缓存。当用户安装后首次打开应用时,直接调用本地缓存数据渲染页面,跳过网络请求环节。优质儿童内容品牌“凯叔讲故事”非常重视用户体验,接入安装预加载后,页面加载提速37.9%。 实际案例应用:凯叔讲故事 二、周期性预加载:动态场景即发即现周期性预加载服务适用于任意页面加载提速的场景,可与安装预加载结合使用。系统每隔12小时拉取一次指定页面(不局限首开页面)的云侧数据并将其缓存到本地,并可将静态资源放置到云端,减少包体大小。开发者可在节日活动开始前通过周期性预加载服务提前将主题资源获取到本地,用户访问时直接从本地获取即可,减少了网络请求的时间和带宽消耗,实现即发即现的效果,提升用户体验。三、FastWeb:移动端H5页面的“隐形加速器”FastWeb组件是一个高性能Web容器,针对App中H5页面可以提供预启动、预渲染、预编译、离线资源免拦截注入等能力。其可提前加载高频页面资源并优化渲染流程,有效解决传统H5页面的白屏、卡顿问题。某省政务服务App接入FastWeb后,页面打开速度提升超32.9%。 (上述数据来源于合作伙伴实践反馈,具体效果以实际场景为准)AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给您带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如您有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢您对HUAWEI AppGallery Connect的支持!
-
产品质量不仅是企业的生命线,更是用户体验的第一道门。 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
108 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时,需留意坐标系相关的说明,可快速定界出是否是坐标系的问题
上滑加载中
推荐直播
-
华为云码道 × 仓颉编程:工程化AI编码探索2026/05/27 周三 19:00-21:00
刘俊杰-华为云仓颉语言专家/李炎-华为云码道技术专家/王智鹏-OpenCangjie开源社区发起人
本场直播围绕华为云仓颉语言与华为云码道的深度结合,展示华为云智能编程从零基础到高效落地的完整生态能力。以华为云码道为引擎,仓颉语言为载体,带给大家日常提效、趣味创新到极速量产的开发体验。
回顾中
热门标签