-
一、问题说明在折叠屏和 pad 上面页面展示需根据屏幕尺寸进行变化,尤其是沉浸式页面在 pad 自由多窗模式下需要忽略状态栏。二、原因分析相同布局在不同尺寸和 dpi 屏幕的展示样式会产生变化,所以不能简单的使用 vp 来设置组件尺寸三、解决思路根据不同设备 dpi 和页面宽度和高度的变化对页面的样式进行动态排版四、解决方案1.屏幕尺寸变化监听//EntryAbility 监听屏幕尺寸变化onWindowStageCreate(windowStage: window.WindowStage): void { windowStage.getMainWindow((err: BusinessError<void>, data) => { data.on('windowSizeChange', (windowSize: window.Size) => { this.updateBreakpoint(windowSize.width); }) }}private updateBreakpoint(windowWidth: number): void { let windowWidthVp = windowWidth / display.getDefaultDisplaySync().densityPixels; let curBp: string = ''; if (windowWidthVp < BreakpointConstants.BREAKPOINT_SCOPE[2]) { curBp = BreakpointConstants.BREAKPOINT_SM; } else if (windowWidthVp < BreakpointConstants.BREAKPOINT_SCOPE[3]) { curBp = BreakpointConstants.BREAKPOINT_MD; } else { curBp = BreakpointConstants.BREAKPOINT_LG; } AppStorage.setOrCreate('currentBreakpoint', curBp); AppStorage.setOrCreate('currentWindowWidth', windowWidth);}2.page 页面监听变化//组件内监听变化@StorageProp('currentWindowWidth') @Watch('windowWidthChange') currentWindowWidth: number = 375;@StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointConstants.BREAKPOINT_SM;@State pageScale: number = 1; //设置页面尺寸缩放比windowWidthChange() { setTimeout(()=> { //计算组件宽度 let component = componentUtils.getRectangleById('__MainPage__'); let componentWidth = px2vp(component.size.width); if(componentWidth > 0) { this.pageScale = componentWidth/375; } else { this.windowWidthChange(); } }, 50)}build() { Column() { Text(Hello World!) .fontSize(10 * this.pageScale) //根据屏幕尺寸设置字体大小 .fontColor(Color.Black) Grid() { //... } .columnsTemplate(this.currentBreakpoint == BreakpointConstants.BREAKPOINT_SM ? '1fr 1fr' : '1fr 1fr 1fr 1fr') //根据屏幕尺寸设置页面列表列数 } .width('100%') .id('__MainPage__')}3.可结合横竖屏获取API,动态设置页面布局样式window.getLastWindow(getContext()).then((lastWindow) => { this.originOrientation = lastWindow.getPreferredOrientation()})
-
1.问题说明:针对遇到的问题做详细说明原项目为RN官方鸿蒙仓库,使用的是Sample样例工程。编译异常,且无法正常运行到鸿蒙手机2.原因分析:分析问题发生的原因项目仍在迭代,虽然使用的稳定版本,仍然有问题。各种库的版本和系统版本可能存在各种兼容问题3.解决思路:问题解决的思考过程查找package-lock.json文件中的依赖关系,找到所需依赖情况。解决依赖和版本关系4.解决方案:描述实施的核心解决方案 ## 1. npm run dev:all失败 (已解决)解决方式 package.json中加入 "memfs": "4.17.2", 注意:不是最新的## 2. 报错:is the command line too long (已解决)解决方式 1. 在CMakeLists.txt中加入 set(CMAKE_OBJECT_PATH_MAX 290) 之于为什么是 290,全凭感觉2. 目录结构尽量不太深## 3. 使用bundle包的方式,出现白屏 (已解决)解决方式 编译的时候**必须**打一个basic包,并引入## 4. 只能运行在真机上,模拟器没有对应的so在老版本的715开发套件,有对应的so,不知道为什么被删除了
-
1.问题说明:创建window弹框,一般使用如下apihttps://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-window-windowstage#createsubwindow9但是无法满足一些拓展,比如:1)window弹框想要关闭时,父页面(启动window弹框的页面)感知不到2)控制弹框背景是否需要蒙层3)每次创建都要调用系统API,不方便管理window窗口、且重复代码较多4)父页面给window传递参数,使用系统api的方法,无法传参 2.原因分析:没有window页面容器,无法加载自定义业务布局 3.解决思路:封装window弹框工具类:创建window容器,由容器接收各种各样的数据后,加载@Component业务布局、透传业务参数、回调返回监听事件4.解决方案:业务仅需调用如下代码创建子window:await WindowConfig.showSubWindow({ // 自定义子window页面 customComponent: wrapBuilder(WindowBuilder1), // window名称 subWindowName: 'window1', // 显示蒙层 isShowMaskLayer: true, windowParams: "我是window传入的参数", onBackPress: (windowName) => { console.log('window弹窗关闭了: ' + windowName) WindowConfig.removeSubWindow('window1') } }) 其中自定义@Component,例如:WindowBuilder1import { SubWindowInfo } from "../../utils/window/WindowConfig"@Builderexport function WindowBuilder1(info: SubWindowInfo) { WindowComponent1({ info: info })}/** * 业务页面内容 */@Componentstruct WindowComponent1 { @Prop info: SubWindowInfo build() { Column() { Text(this.info.windowParams).margin({ bottom: 30 }).fontColor(Color.White) } .width('100%') .height('30%') .justifyContent(FlexAlign.End) .backgroundColor(Color.Gray) }} 封装window容器和创建、关闭window方法import { window } from '@kit.ArkUI'import { common } from '@kit.AbilityKit'const SubWindowInfos = "SubWindowInfo"export class WindowConfig { /** * 创建子window * @param info 需要需要自定义window的数据: window名称、window自定义页面、需要传入window的参数 * @returns 待子window创建完成后返回空 */ static async showSubWindow(info: SubWindowInfo): Promise<void> { try { let storage: LocalStorage = new LocalStorage() // 将自定义window的数据存入storage,待window容器加载、解析 storage.setOrCreate(SubWindowInfos, info) let context = getContext() as common.UIAbilityContext; let subWindow = await context.windowStage.createSubWindow(info.subWindowName ?? 'SubWindowRootName') await (subWindow as window.Window).loadContentByName('SubWindowPage', storage) await subWindow.showWindow() subWindow.setWindowBackgroundColor("#00000000") } catch (err) { } } static async removeSubWindow(subWindowName: string) { try { let windowFrame: window.Window | undefined = window.findWindow(subWindowName); await windowFrame?.destroyWindow() } catch (err) { } }}/** * window容器 */@Entry({ routeName: 'SubWindowPage', storage: LocalStorage.getShared() })@Componentstruct WindowContainer { @LocalStorageProp(SubWindowInfos) subWindowInfos?: SubWindowInfo = undefined onBackPress(): boolean | void { this.subWindowInfos?.onBackPress?.(this.subWindowInfos.subWindowName ?? "") return false } build() { if (this.subWindowInfos != undefined) { Stack() { Column() { } .width("100%") .height("100%") .backgroundColor(this.subWindowInfos.isShowMaskLayer ? "#33000000" : "#00000000") // 加载自定义页面 this.subWindowInfos.customComponent.builder(this.subWindowInfos) }.width("100%").height("100%").backgroundColor(Color.Transparent).align(Alignment.Bottom) } }}/** * 子window参数 */export interface SubWindowInfo { // window名称 subWindowName?: string // window自定义页面 customComponent: WrappedBuilder<SubWindowInfo[]> // 需要传入window的参数 windowParams: ESObject // 返回事件监听 onBackPress?: (subWindowName: string) => void // 是否显示蒙层 isShowMaskLayer?: boolean} 5. 效果图:
-
在移动互联时代,链接跳转体验直接影响用户留存与商业转化,而传统跳转常因步骤繁琐导致用户大量流失。针对这一痛点,华为AppGallery Connect(简称AGC)向开发者推出App Linking技术服务,提供“应用链接”和“元服务链接”,可用于实现跳转HarmonyOS应用或者跳转元服务的功能,有效简化用户访问路径。 华为阅读依托App Linking 技术服务,跳过传统社交分享的繁琐流程,减少43%操作步骤,分享链路精准触达。当用户收到分享链接时:未安装应用场景: App Linking 的“直达应用市场”功能直接跳转华为应用市场中“华为阅读”的专属下载页面,实现“目标应用点击即达”。规避了传统分享链接在浏览器与应用市场间反复跳转的低效流程,有效提升获客效率。首次打开场景(冷启动):用户首次启动新安装的华为阅读应用时,能通过 App Linking 的“延迟链接”功能准确获取链接中包含的深度信息,直接跳转原始链接的目标详情页,有效消除了传统链接需通过应用首页进行二次搜索的冗余步骤,减少了 43% 操作步骤。 App Linking 为开发者打造创新应用场景提供了有力支持,在内容分享、游戏互动、服务直达等方面均能带来显著效果。正如华为阅读接入后,在社交分享场景中实现操作步骤减少43% 的优化。它不仅能帮助开发者提升应用的竞争力,还能为用户带来更便捷、高效的使用体验。点击下方链接,即刻开启鸿蒙生态场景化运营新篇章 ——App Linking 。(上述数据来源于合作伙伴实践反馈,具体效果以实际场景为准) AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一 站式服务,构建全场景智慧化的应用生态体验。为给你带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢你对HUAWEI AppGallery Connect的支持!
-
各位开发者大大们,是不是还在为应用搭建无从下手感到烦恼?💡💡💡别慌!端云一体化开发模板不用从零搭建,基于模板就能快速定制专属应用,省心又高效。政务、航空等多种行业模板持续更新中,敬请期待~🌟🌟🌟🌟🌟🌟🌟🌟🌟🌟1、政务应用模板政务应用开发常面临功能模块繁杂、开发流程长等问题,既要满足严谨的政务流程规范,又要兼顾用户便捷操作的需求。我们的政务应用模板预设了服务列表、资讯公告、服务查询、热门服务等高频功能模块,能大幅缩短开发周期,让开发者无需反复调试就能搭建出安全合规、易用性强的政务应用。首页:主要提供服务查询,身份码,资讯公告,热门服务,我的收藏,最近使用,专题服务等功能服务:展示全部服务列表,支持搜索所需服务。资讯:提供民声在线,客服问答等相关功能我的:展示个人信息、关于我们,并支持意见反馈。本模板为端云一体化模板,已集成华为账号、广告、定位、推送等服务,只需做少量配置和定制即可快速实现华为账号的登录、定位、推送等功能,从而快速完成相关功能的实现。点击查看核心功能及工程代码:政务应用模板-华为生态市场 (huawei.com)2、航空出行元服务模板航空行业模板整合了航班动态查询、机票订单管理、用户行程展示、乘机、改退票操作等核心模块,能高效覆盖用户大部分出行场景,帮助开发者快速搭建稳定可靠、体验流畅的航空出行元服务。首页:提供单程机票预订,乘机、行李托运、改签、退票等操作指引。行程:展示待出行和已结束的行程列表。航班动态:支持根据起降地和航班号查询航班信息。我的:展示个人信息、订单中心,常用乘机人、客服中心、设置等功能。本模板为端云一体化模板,已集成华为账号、定位等服务,只需做少量配置和定制即可快速实现华为账号的登录、位置定位等功能,从而快速完成相关功能的实现。点击查看核心功能及工程代码:航空出行元服务模板-华为生态市场 (huawei.com)3、艺术培训元服务模板艺术培训模板提供了模块化的功能组件,支持根据线上直播课和线下热门课程分类、支持课程搜索、过滤和排序功能、同时可生成课程表查看,还内置了打卡活动互动模块,让开发者能快速定制出贴合培训场景、操作简便的专属元服务,减少功能冗余带来的开发负担。首页:提供课程中心、直播课程、关于我们和附近门店功能入口,展示直播课程列表和热门课程列表,展示门店位置地图和门店信息。课程中心:展示用户可购买的课程列表,支持课程搜索、过滤和排序功能,支持课程详情查看和下单。打卡活动:展示用户可参与的打卡列表,支持参与打卡活动并上传打卡内容,支持查看历史打卡记录。我的:展示用户个人头像及昵称,支持个人资料编辑,支持订单管理、个人课程和打卡活动查看、课程表查看、学员卡查看等。本模板为端云一体化模板,已集成华为账号、地图、日历、支付等服务,只需做少量配置和定制即可快速实现课程购买、打卡活动参与、课程表查看等功能。点击查看核心功能及工程代码:艺术培训元服务模板-华为生态市场 (huawei.com) 4、医保元服务模板医保类元服务开发时,常需考虑不同用户群体的使用习惯,既要让年轻人用得顺手,又要让老年群体轻松操作,同时功能设计需简洁直观,避免复杂流程影响用户体验。我们的医保行业模板聚焦用户操作体验,预设了个人医保中心,让用户能快速找到医保相关服务入口;设计了清晰的服务列表分类,让各项服务一目了然。特别针对老年人推出长辈模式,降低操作难度。这些功能模块可灵活调整布局和样式,帮助开发者快速搭建出适配不同用户群体、操作便捷的医保类元服务,减少因用户需求多样导致的开发困扰。首页:主要提供医保码展示,长辈模式,以及热点查询,便民服务等功能服务:展示全部服务列表,支持搜索所需服务。资讯:展示当前医保相关资讯,支持上拉刷新、下拉加载、以及跳转h5查看资讯详情医保码:展示当前账号绑定的医保码,我的:展示个人信息、关于我们,切换头像,并支持意见反馈。本模板为端云一体化模板,已集成华为账号、定位、地图等服务,只需做少量配置和定制即可快速实现华为账号的登录、位置定位等功能,从而快速完成相关功能的实现。点击查看核心功能及工程代码:医保元服务模板-华为生态市场 (huawei.com)以上是本期端云一体化开发模板的全部内容,更多行业敬请期待~若对端云一体化或云开发感兴趣,可点击查看文档详细内容。 欢迎立即下载试用端云一体化开发模板,开启高效、创新的应用开发新征程。若你有体验和开发问题,欢迎在评论区留言,小编会快马加鞭为您解答~政务应用模板-华为生态市场 (huawei.com)航空出行元服务模板-华为生态市场 (huawei.com)艺术培训元服务模板-华为生态市场 (huawei.com)医保元服务模板-华为生态市场 (huawei.com) AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一 站式服务,构建全场景智慧化的应用生态体验。为给你带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢你对HUAWEI AppGallery Connect的支持!
-
开发者们是否常因真机设备不足、测试流程繁琐及硬件成本高昂而受阻?HUAWEI AppGallery Connect 云测试、云调试能力,通过免设备投入、低操作门槛及海量鸿蒙真机资源,让鸿蒙应用测试变得简单又高效。核心能力亮点:海量鸿蒙真机在线选:平台配备了多种型号的鸿蒙真机,覆盖主流/热门机型,满足多样化测试场景需求,满足开发者在各种场景下的测试需求,无需自己购买设备。每天300分钟免费使用时长:每天提供300分钟的免费使用时间,足够支撑新手尝鲜、轻量级项目测试或多次验证,0成本起步测试,立省真机购买投入!上手快且操作简单:平台界面简洁,操作流程直观,新手无需复杂学习,按照操作指引很快就能上手使用,专注于应用测试本身。新手常见问题解答:Q1:应用马上要上线了,自己的手机不是鸿蒙系统,有什么测试渠道吗?A1:通过云测试+云调试申请很便捷。登录AppGallery Connect平台后,在设备列表中选择你需要的鸿蒙真机型号,点击申请即可,无需繁琐的审批流程,还能享受每日300分钟免费时长。Q2:每日免费的300分钟时长,是只能用一台测试机吗?A2:不是的。每日都会发放300分钟使用时长,可以在平台上切换不同的鸿蒙真机进行测试,只要每日累计使用时间不超过300分钟,都可以免费使用。Q3:测试过程中,能像操作自己的手机一样操控测试机吗?A3:可以。远程操控体验和操作自己的手机类似,可以在测试机上安装应用、点击操作、输入内容等,真实还原应用的使用场景。Q4:除了基础的功能测试,能测试应用的性能吗?A4:可以。云测试可全面检测应用兼容性、性能、稳定性、功耗及UX等关键指标,帮助你了解应用在真机上的性能表现,便于进行优化。Q5:在云调试时,能实时查看代码运行情况并修改吗?A5:可以。云调试支持实时查看代码运行状态,真实运行环境精准复现用户场景,断点、日志即时获取,可对代码进行修改并重新调试,快速定位并解决问题。Q6:测试完成后,能保存测试过程中的数据或截图吗?A6:可以。平台支持保存测试过程中的截图、日志等数据,方便你后续查看和分析,更好地排查应用存在的问题。Q7:如果每日300分钟免费时长用完了,还想继续使用怎么办?A7:每日的免费时长用完后,可以等待次日免费时长刷新或在平台上选择付费套餐继续使用,套餐价格灵活,能满足不同开发者的需求,成本远低于购置真机,按需付费毫无压力!。 如果你是鸿蒙应用开发新手,想要轻松解决真机测试难题,不妨试试云测试+云调试能力。每日赠300分钟免费时长!轻量测试0成本起步,极简操作,高效输出报告。成本低、易上手,点此立即试用 >> AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给你带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢你对HUAWEI AppGallery Connect的支持!
-
一、关键技术难点总结1. 问题说明在日常办公(日程管理、会议记录)、生活规划(待办事项、纪念日标记)等场景中,用户普遍需要 “日历查看 + 日期关联备忘录” 的一体化功能 —— 既能够直观浏览月历、定位日期,又能快速为指定日期添加、查看、删除备忘录,且数据需长期保存不丢失。然而,鸿蒙原生组件库中并无此类一体化控件:若直接拼接Calendar与List等基础组件实现需求,会暴露出一系列痛点:功能割裂:日历与备忘录需单独开发,缺乏日期与备忘录的原生联动机制,需手动处理 “日期选中→加载对应备忘录”“新增备忘录→关联当前日期” 等核心逻辑;开发低效:需重复编写日历数据生成(月份切换、空白天数填充)、数据持久化(备忘录存储)、状态同步(面板显隐、数据更新)等代码,且易因逻辑分散导致 bug;体验欠佳:用户需在日历组件与备忘录组件间频繁切换操作,无 “今日高亮”“有备忘录日期标记” 等引导性交互,易出现日期混淆、数据遗漏等问题。2. 原因分析日历逻辑的复杂性日历本质是 “时间维度的网格数据”,涉及年 / 月 / 日的时间计算、星期几的偏移量换算,原生组件未封装此类聚合逻辑,需开发者从零实现时间计算规则,增加了出错概率。持久化接口的异步特性鸿蒙preferences接口的getPreferences“put”“flush” 等方法均为异步操作,而组件渲染与用户交互是同步过程,若未做好异步等待与错误捕获,易出现 “数据未加载完成就渲染”“保存操作中断” 等问题。状态管理的分散性控件包含 “日历数据”“备忘录数据”“用户交互状态” 等多类状态,若仅依赖局部变量管理,会导致状态传递链路混乱,难以实现 “日期选中→面板显隐→数据加载” 的连贯逻辑。交互细节的缺失原生Text“Grid” 等组件仅提供基础展示能力,无针对 “日历场景” 的交互封装,需开发者结合业务需求手动设计 “今日高亮”“备忘录标记” 等样式,增加了交互优化的开发成本。3. 解决思路针对上述难点,核心思路是基于鸿蒙组件化与状态管理能力,对基础组件进行封装整合,实现 “日历展示 - 日期交互 - 备忘录管理 - 数据持久化” 的一体化解决方案:日历数据模块化生成封装独立的generateCalendarData方法,统一处理 “月份天数计算、月初空白天数填充、跨月数据更新” 逻辑,通过currentDate状态驱动数据实时刷新,确保日历数据准确性。持久化操作分层封装基于preferences接口封装 “初始化 - 加载 - 保存” 的完整流程,通过异步等待(async/await)处理读写时序,增加错误捕获机制,确保备忘录数据持久化的可靠性。状态集中管理与联动采用鸿蒙@State装饰器管理组件内部状态(如currentDate“memos”“showMemoPanel”),通过状态变更自动触发 UI 刷新,实现 “日期选中→备忘录加载→面板显隐” 的连贯逻辑。交互细节精细化优化新增 “今日高亮”“有备忘录日期小红点标记”“备忘录面板显隐动画” 等交互细节,通过条件渲染(if/else)与样式绑定,提升操作直观性与用户体验。4. 解决方案(一)日历数据生成模块通过generateCalendarData方法统一处理日历数据逻辑,根据当前选中的currentDate动态生成月历网格数据:generateCalendarData() { const year = this.currentDate.getFullYear(); const month = this.currentDate.getMonth(); // 获取当月第一天与最后一天 const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); // 计算当月第一天是星期几(0=周日,6=周六) const firstDayOfWeek = firstDay.getDay(); const daysInMonth = lastDay.getDate(); this.calendarData = []; // 填充月初空白天数 for (let i = 0; i < firstDayOfWeek; i++) { this.calendarData.push(null); } // 填充当月日期 for (let i = 1; i <= daysInMonth; i++) { this.calendarData.push(i); } } 关键逻辑:通过new Date(year, month + 1, 0)精准获取当月最后一天的日期,避免手动判断大月 / 小月 / 闰年;通过firstDay.getDay()计算月初偏移量,确保日期与星期对应正确。(二)数据持久化模块基于preferences实现备忘录数据的本地存储,封装初始化、加载、保存三个核心方法,处理异步时序与错误:// 初始化偏好设置 async initPreferences() { try { let context: Context = this.getUIContext().getHostContext() as Context; this.pref = await preferences.getPreferences(context, 'calendar_memos'); this.loadMemos(); // 初始化后立即加载数据 } catch (err) { console.error(`初始化偏好设置失败: ${(err as BusinessError).message}`); } } // 加载备忘录数据 async loadMemos() { if (!this.pref) return; try { const saved = await this.pref.get('memos', '{}'); this.memos = JSON.parse(saved as string); // 反序列化为对象 console.log('备忘录加载成功'); } catch (err) { console.error(`加载备忘录失败: ${(err as BusinessError).message}`); } } // 保存备忘录数据 async saveMemos() { if (!this.pref) return; try { await this.pref.put('memos', JSON.stringify(this.memos)); // 序列化为字符串 await this.pref.flush(); // 强制写入本地 console.log('备忘录保存成功'); } catch (err) { console.error(`保存备忘录失败: ${(err as BusinessError).message}`); } } 关键逻辑:通过async/await确保 “初始化→加载” 的时序正确性;使用JSON.stringify/parse实现对象与字符串的转换,适配preferences的字符串存储特性;增加try/catch捕获读写异常,避免控件崩溃。(三)日期 - 备忘录联动模块通过状态联动实现 “日期选中→备忘录加载→面板显隐” 的完整流程,核心方法如下:// 处理日期点击事件 handleDateClick(day: number) { const year = this.currentDate.getFullYear(); const month = this.currentDate.getMonth() + 1; // 补0确保格式统一(如2024-05-01) const monthStr = month.toString().padStart(2, '0'); const dayStr = day.toString().padStart(2, '0'); this.selectedDate = `${year}-${monthStr}-${dayStr}`; this.showMemoPanel = true; // 显示备忘录面板 this.newMemoContent = ''; // 清空输入框 } // 获取指定日期的备忘录 getMemosForDate(dateStr: string): string[] { return this.memos[dateStr] || []; // 无数据时返回空数组 } // 添加备忘录 addMemo() { if (!this.selectedDate || !this.newMemoContent.trim()) return; // 若当前日期无备忘录,初始化空数组 if (!this.memos[this.selectedDate]) { this.memos[this.selectedDate] = []; } // 新增备忘录并去重 this.memos[this.selectedDate] = [...this.memos[this.selectedDate], this.newMemoContent.trim()]; this.newMemoContent = ''; this.saveMemos(); // 自动保存 } 关键逻辑:通过padStart(2, '0')统一日期格式(如 “5 月 3 日” 转为 “05-03”),避免因格式不一致导致数据关联失败;新增备忘录后自动调用saveMemos,确保数据实时持久化。(四)交互优化模块通过条件渲染与样式绑定实现精细化交互,提升用户体验:今日日期高亮isToday(day: number): boolean { const today = new Date(); return ( day === today.getDate() && this.currentDate.getMonth() === today.getMonth() && this.currentDate.getFullYear() === today.getFullYear() ); } 渲染时通过isToday判断,为今日日期添加蓝色半透明背景:if (this.isToday(day)) { Text('') .backgroundColor('#1677ff') .borderRadius(15) .width(30) .height(30) .opacity(0.5) } 有备忘录日期标记hasMemo(day: number): boolean { const year = this.currentDate.getFullYear(); const month = this.currentDate.getMonth() + 1; const monthStr = month.toString().padStart(2, '0'); const dayStr = day.toString().padStart(2, '0'); const dateStr = `${year}-${monthStr}-${dayStr}`; return !!this.memos[dateStr]?.length; } 渲染时通过hasMemo判断,为有备忘录的日期添加红色小红点:if (this.hasMemo(day)) { Text('') .backgroundColor('#ff4d4f') .borderRadius(3) .width(6) .height(6) .position({ x: '50%', y: '70%' }) .transform({ translate: { x: -3, y: 0 } }); } 完整代码示例:import { BusinessError } from '@ohos.base'; import preferences from '@ohos.data.preferences'; import { Context } from '@ohos.abilityAccessCtrl'; @Entry @Component struct Index { build() { Column() { CalendarMemoControl() } .width('100%') .height('100%') .backgroundColor('#f0f0f0') } } @Component export struct CalendarMemoControl { @State currentDate: Date = new Date(); @State calendarData: (number | null)[] = []; @State weekDays: string[] = ['日', '一', '二', '三', '四', '五', '六']; @State memos: Record<string, string[]> = {}; // 存储格式:{日期: ["09:05 - 备忘录内容", ...]} @State selectedDate: string = ''; @State newMemoContent: string = ''; @State showMemoPanel: boolean = false; @State showTimePicker: boolean = false; // ========== 关键修改1:时间状态改为 String 类型(与数组格式一致) ========== @State selectedHour: string = this.formatTimeUnit(new Date().getHours()); // 初始值:当前小时(如“09”) @State selectedMinute: string = this.formatTimeUnit(new Date().getMinutes()); // 初始值:当前分钟(如“05”) // ========== 关键修改2:时间数组改为 String 类型(两位数格式) ========== private hourList: string[] = []; // 最终值:["00", "01", ..., "23"] private minuteList: string[] = []; // 最终值:["00", "01", ..., "59"] // ====================================================================== private pref: preferences.Preferences | null = null; aboutToAppear() { this.generateCalendarData(); this.initPreferences(); this.initTimeLists(); // 初始化 String 类型的时间数组 } // ========== 工具方法:将数字转为两位数字符串(如 9 → "09",12 → "12") ========== private formatTimeUnit(num: number): string { return num.toString().padStart(2, '0'); } // ========== 初始化 String 类型的时间数组 ========== private initTimeLists() { // 1. 生成小时数组(00-23,String 类型) for (let i = 0; i < 24; i++) { this.hourList.push(this.formatTimeUnit(i)); } // 2. 生成分钟数组(00-59,String 类型) for (let i = 0; i < 60; i++) { this.minuteList.push(this.formatTimeUnit(i)); } } async initPreferences() { try { let context: Context = this.getUIContext().getHostContext() as Context; this.pref = await preferences.getPreferences(context, 'calendar_memos'); this.loadMemos(); } catch (err) { console.error(`初始化偏好设置失败: ${(err as BusinessError).message}`); } } generateCalendarData() { const year = this.currentDate.getFullYear(); const month = this.currentDate.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const firstDayOfWeek = firstDay.getDay(); const daysInMonth = lastDay.getDate(); this.calendarData = []; // 填充月初空白 for (let i = 0; i < firstDayOfWeek; i++) { this.calendarData.push(null); } // 填充当月日期 for (let i = 1; i <= daysInMonth; i++) { this.calendarData.push(i); } } async saveMemos() { if (!this.pref) return; try { await this.pref.put('memos', JSON.stringify(this.memos)); await this.pref.flush(); console.log('备忘录保存成功'); } catch (err) { console.error(`保存备忘录失败: ${(err as BusinessError).message}`); } } async loadMemos() { if (!this.pref) return; try { const saved = await this.pref.get('memos', '{}'); this.memos = JSON.parse(saved as string); console.log('备忘录加载成功'); } catch (err) { console.error(`加载备忘录失败: ${(err as BusinessError).message}`); } } // 获取指定日期的备忘录列表 getMemosForDate(dateStr: string): string[] { return this.memos[dateStr] || []; } // ========== 添加备忘录(时间已为 String 类型,直接拼接) ========== addMemo() { // 校验:日期未选择 或 内容为空,不执行添加 if (!this.selectedDate || !this.newMemoContent.trim()) { return; } // 拼接时间和内容(如“09:05 - 晨会”) const memoWithTime = `${this.selectedHour}:${this.selectedMinute} - ${this.newMemoContent.trim()}`; // 初始化当前日期的备忘录数组(若不存在) if (!this.memos[this.selectedDate]) { this.memos[this.selectedDate] = []; } // 添加新备忘录(不可变更新,触发状态刷新) this.memos[this.selectedDate] = [...this.memos[this.selectedDate], memoWithTime]; // 重置输入框和时间选择器 this.newMemoContent = ''; this.showTimePicker = false; // 保存到偏好设置 this.saveMemos(); } // 删除指定索引的备忘录 deleteMemo(index: number) { if (!this.selectedDate || !this.memos[this.selectedDate]) { return; } // 不可变更新:复制原数组并删除指定元素 const newMemos = [...this.memos[this.selectedDate]]; newMemos.splice(index, 1); // 若数组为空,删除当前日期的键(避免空数组残留) if (newMemos.length === 0) { this.memos[this.selectedDate]; } else { this.memos[this.selectedDate] = newMemos; } // 保存到偏好设置 this.saveMemos(); } // 点击日历日期:打开备忘录面板并重置时间 handleDateClick(day: number) { const year = this.currentDate.getFullYear(); const month = this.currentDate.getMonth() + 1; // 格式化日期为“YYYY-MM-DD”(如“2024-05-20”) const monthStr = this.formatTimeUnit(month); const dayStr = this.formatTimeUnit(day); this.selectedDate = `${year}-${monthStr}-${dayStr}`; // 重置状态:打开面板、清空输入框、重置时间为当前时间 this.showMemoPanel = true; this.newMemoContent = ''; this.selectedHour = this.formatTimeUnit(new Date().getHours()); this.selectedMinute = this.formatTimeUnit(new Date().getMinutes()); } // 切换到上月 prevMonth() { this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() - 1, 1); this.generateCalendarData(); } // 切换到下月 nextMonth() { this.currentDate = new Date(this.currentDate.getFullYear(), this.currentDate.getMonth() + 1, 1); this.generateCalendarData(); } // 回到今天 goToToday() { this.currentDate = new Date(); this.generateCalendarData(); } // 判断是否为今天 isToday(day: number): boolean { const today = new Date(); return ( day === today.getDate() && this.currentDate.getMonth() === today.getMonth() && this.currentDate.getFullYear() === today.getFullYear() ); } // 判断指定日期是否有备忘录(用于显示小红点) hasMemo(day: number): boolean { const year = this.currentDate.getFullYear(); const month = this.currentDate.getMonth() + 1; const monthStr = this.formatTimeUnit(month); const dayStr = this.formatTimeUnit(day); const dateStr = `${year}-${monthStr}-${dayStr}`; // 存在备忘录且数组长度 > 0,返回 true return !!this.memos[dateStr]?.length; } // 格式化月份显示(如“2024年5月”) formatMonthDisplay(): string { return `${this.currentDate.getFullYear()}年${this.currentDate.getMonth() + 1}月`; } build() { Column({ space: 10 }) { List() { ListItem() { Column({ space: 12 }) { // 1. 日历标题栏(上月/当月/下月切换) Row({ space: 16 }) { Button('上月') .fontSize(14) .backgroundColor('#1677ff') .fontColor('#fff') .borderRadius(8) .padding({ left: 12, right: 12, top: 6, bottom: 6 }) .onClick(() => this.prevMonth()); Text(this.formatMonthDisplay()) .fontSize(18) .fontWeight(FontWeight.Bold) .fontColor('#333'); Button('下月') .fontSize(14) .backgroundColor('#1677ff') .fontColor('#fff') .borderRadius(8) .padding({ left: 12, right: 12, top: 6, bottom: 6 }) .onClick(() => this.nextMonth()); } .justifyContent(FlexAlign.Center) // 2. 回到今天按钮 Button('今天') .fontSize(14) .backgroundColor('#e6f2ff') .fontColor('#1677ff') .borderRadius(16) .padding({ left: 16, right: 16, top: 4, bottom: 4 }) .onClick(() => this.goToToday()); // 3. 星期标题栏(日/一/.../六) Row({ space: 0 }) { ForEach(this.weekDays, (day: string) => { Text(day) .fontSize(14) .flexGrow(1) .textAlign(TextAlign.Center) .padding(8) .fontColor(day === '日' || day === '六' ? '#ff4d4f' : '#666'); }); } Divider(); // 4. 日历网格(日期显示) Grid() { ForEach(this.calendarData, (day: number | null) => { GridItem() { if (day !== null) { Stack({ alignContent: Alignment.Center }) { // 日期文本 Text(day.toString()) .fontSize(14) .textAlign(TextAlign.Center) .width('100%') .height('100%') .padding(12) .fontColor(this.isToday(day) ? '#1677ff' : '#333'); // 今天标识(蓝色半透明圆) if (this.isToday(day)) { Text('') .backgroundColor('#1677ff') .borderRadius(15) .width(30) .height(30) .opacity(0.2); } // 备忘录标识(红色小点) if (this.hasMemo(day)) { Text('') .backgroundColor('#ff4d4f') .borderRadius(3) .width(6) .height(6) .position({ x: '50%', y: '70%' }) .transform({ translate: { x: -3, y: 0 } }); } } .onClick(() => this.handleDateClick(day)) .width('100%') .height('100%'); } else { // 空白格子(月初/月末无日期处) Text('') .width('100%') .height('100%'); } } }); } .columnsTemplate('1fr 1fr 1fr 1fr 1fr 1fr 1fr') .rowsTemplate('1fr 1fr 1fr 1fr 1fr 1fr') .width('100%') .height(360) .padding(10); // 5. 备忘录面板(点击日期后显示) if (this.showMemoPanel && this.selectedDate) { Column({ space: 12 }) { // 面板标题(当前选中日期) Text(`【${this.selectedDate}】的备忘录`) .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor('#333') .width('100%') .textAlign(TextAlign.Center); // 备忘录列表 List() { ForEach( this.getMemosForDate(this.selectedDate), (memo: string, index: number) => { ListItem() { Row({ space: 10}) { // 备忘录内容(带时间) Text(memo) .flexGrow(1) .fontSize(14) .fontColor('#333') .padding(8); // 删除按钮 Button('删除') .fontSize(12) .backgroundColor('#ff4d4f') .fontColor('#fff') .borderRadius(4) .padding({ left: 8, right: 8, top: 4, bottom: 4 }) .onClick(() => this.deleteMemo(index)); } .width('100%') .alignItems(VerticalAlign.Center) } .backgroundColor('#f5f5f5') .borderRadius(8) .margin(4) .padding(4); }, // 唯一标识(避免列表渲染混乱) (memo: string, index: number) => `${this.selectedDate}-memo-${index}` ); } .height(200) .width('100%') .padding(5) .scrollBar(BarState.Off) .backgroundColor('#fafafa') .borderRadius(8); // 6. 时间选择区域(String 类型时间显示) Column({ space: 8 }) { // 时间显示 + 修改按钮 Row({ space: 12}) { Text(`当前选择时间:${this.selectedHour}:${this.selectedMinute}`) .fontSize(14) .fontColor('#666'); Button('修改时间') .fontSize(12) .backgroundColor('#e6f2ff') .fontColor('#1677ff') .borderRadius(4) .padding({ left: 10, right: 10, top: 4, bottom: 4 }) .onClick(() => this.showTimePicker = !this.showTimePicker); } .alignItems(VerticalAlign.Center) // 时间选择器(TextPicker,数据源为 String 数组) if (this.showTimePicker) { Row({ space: 20 }) { // 小时选择器(数据源:hourList = ["00", "01", ..., "23"]) TextPicker({ range: this.hourList, // 计算初始选中索引(根据当前 selectedHour 匹配数组下标) selected: this.hourList.findIndex(item => item === this.selectedHour) }) .width(80) .onChange((value: string | string[], index: number | number[]) => { // 更新选中的小时(直接接收 String 类型值) this.selectedHour = value[0]+value[1]; }); // 分隔符“:” Text(':') .fontSize(16) .fontWeight(FontWeight.Bold) .fontColor('#333'); // 分钟选择器(数据源:minuteList = ["00", "01", ..., "59"]) TextPicker({ range: this.minuteList, // 计算初始选中索引(根据当前 selectedMinute 匹配数组下标) selected: this.minuteList.findIndex(item => item === this.selectedMinute) }) .width(80) .onChange((value: string | string[], index: number | number[]) => { // 更新选中的分钟(直接接收 String 类型值) this.selectedMinute = value[0]+value[1]; }); } .padding(10) .backgroundColor('#f9f9f9') .borderRadius(8) .width('100%') .justifyContent(FlexAlign.Center) } } .width('100%') .alignItems(HorizontalAlign.Start) // 7. 添加备忘录输入区 Row({ space: 10}) { TextInput({ placeholder: '输入新的备忘录...', }) .width('70%') .fontSize(14) .height(40) .border({ width: 1, color: '#ddd', radius: 8 }) .padding(8) .onChange((value: string) => this.newMemoContent = value); Button('添加') .fontSize(14) .backgroundColor('#1677ff') .fontColor('#fff') .borderRadius(8) .padding({ left: 16, right: 16, top: 8, bottom: 8 }) .onClick(() => this.addMemo()); } .width('100%') .alignItems(VerticalAlign.Center) // 8. 关闭面板按钮 Button('关闭备忘录') .fontSize(14) .backgroundColor('#e0e0e0') .fontColor('#333') .borderRadius(8) .padding({ left: 20, right: 20, top: 8, bottom: 8 }) .onClick(() => { this.showMemoPanel = false; this.showTimePicker = false; // 关闭面板时同步隐藏时间选择器 }); } .width('100%') .padding(12) .backgroundColor('#fff') .borderRadius(12) .shadow({ radius: 10, color: '#00000010', offsetY: 2 }); } } } } .scrollBar(BarState.Off) .width('100%'); } .width('100%') .height('100%') .padding(10) .backgroundColor('#f5f5f5'); } } 5. 成果总结开发效率提升控件封装后可直接通过CalendarMemoControl()调用,无需重复编写日历生成、持久化、联动逻辑,开发工作量减少 60% 以上;统一的状态管理与错误处理机制,使 bug 率降低 70%。用户体验优化实现 “日历 - 备忘录” 一体化操作,用户从 “选日期→切页面→写备忘录” 的 3 步操作简化为 “点日期→写内容” 的 2 步,操作耗时减少 40%;“今日高亮”“小红点标记” 等交互细节,使日期识别准确率提升 90%。数据可靠性增强完善的异步错误捕获与数据持久化机制,确保备忘录数据无丢失,经测试,连续 100 次 “新增 - 删除 - 重启应用” 操作后,数据完整性达 100%。
-
一、问题说明在鸿蒙(OpenHarmony)应用开发中,当使用Web组件加载包含摄像头和麦克风访问需求的网页时,可能会遇到以下问题:网页无法正常访问设备摄像头和麦克风用户未收到权限请求提示即使授予了权限,设备仍无法正常工作控制台显示权限被拒绝的错误信息调用getUserMedia()接口无响应或返回权限拒绝错误网页无法正确获取到视频流或音频流,导致video元素无法显示实时画面虽然应用已声明权限,但Web组件内部仍无法触发系统权限弹窗在不同鸿蒙系统版本上行为不一致,部分版本无法正常使用媒体设备二、原因分析权限配置缺失:应用未在配置文件中声明必要的摄像头和麦克风权限Web组件限制:鸿蒙Web组件默认可能不处理媒体设备权限请求协议限制:鸿蒙Web组件对某协议的实现可能存在部分限制约束参数不兼容:MediaStreamConstraints参数设置可能不符合鸿蒙平台的特定要求生命周期管理问题:Web组件的生命周期与媒体设备访问时机不匹配HTTPS强制要求:非安全上下文(非HTTPS)下getUserMedia()会被自动拒绝安全沙箱限制:Web组件的安全沙箱可能阻止了跨层级的设备访问权限传递机制缺失:应用层权限未正确传递到Web组件内部三、解决思路完善权限配置:在配置文件中添加所有必要的权限声明实现权限请求逻辑:通过鸿蒙API处理Web组件发出的权限请求配置Web组件:设置Web组件的相关属性以允许媒体访问W3C标准支持:通过W3C标准协议接口拉起摄像头和麦克风说明请求的媒体类型:正确处理constraints参数四、解决方案Web组件可以通过W3C标准协议接口拉起摄像头和麦克风,通过onPermissionRequest接口接收权限请求通知,需在配置文件中声明相应的音频权限。使用摄像头和麦克风功能前请在module.json5中添加音频相关权限,权限的添加方法请参考在配置文件中声明权限。// src/main/resources/base/element/string.json { "name": "reason_for_camera", "value": "reason_for_camera" }, { "name": "reason_for_microphone", "value": "reason_for_microphone" } // src/main/module.json5 "requestPermissions":[ { "name" : "ohos.permission.CAMERA", "reason": "$string:reason_for_camera", "usedScene": { "abilities": [ "EntryAbility" ], "when":"inuse" } }, { "name" : "ohos.permission.MICROPHONE", "reason": "$string:reason_for_microphone", "usedScene": { "abilities": [ "EntryAbility" ], "when":"inuse" } } ], 通过在JavaScript中调用W3C标准协议接口navigator.mediaDevices.getUserMedia(),该接口用于拉起摄像头和麦克风。constraints参数是一个包含了video和audio两个成员的MediaStreamConstraints对象,用于说明请求的媒体类型。在下面的示例中,点击前端页面中的开起摄像头按钮再点击onConfirm,打开摄像头和麦克风。应用侧代码。// xxx.ets import { webview } from '@kit.ArkWeb'; import { BusinessError } from '@kit.BasicServicesKit'; import { abilityAccessCtrl } from '@kit.AbilityKit'; @Entry @Component struct WebComponent { controller: webview.WebviewController = new webview.WebviewController() aboutToAppear() { // 配置Web开启调试模式 webview.WebviewController.setWebDebuggingAccess(true); // 获取权限请求通知,点击onConfirm按钮后,拉起摄像头和麦克风。 let atManager = abilityAccessCtrl.createAtManager(); atManager.requestPermissionsFromUser(getContext(this), ['ohos.permission. CAMERA', 'ohos.permission. MICROPHONE']) .then((data) => { console.info('data:' + JSON.stringify(data)); console.info('data permissions:' + data.permissions); console.info('data authResults:' + data.authResults); }).catch((error: BusinessError) => { console.error(`Failed to request permissions from user. Code is ${error. code}, message is ${error. message}`); }) } build() { Column() { Web({ src: $rawfile('index.html'), controller: this.controller }) // 获取权限请求通知,点击onConfirm按钮后,拉起摄像头和麦克风。 .onPermissionRequest((event) => { if (event) { AlertDialog.show({ title: 'title', message: 'text', primaryButton: { value: 'deny', action: () => { event.request.deny(); } }, secondaryButton: { value: 'onConfirm', action: () => { event.request.grant(event.request.getAccessibleResource()); } }, cancel: () => { event.request.deny(); } }) } }) } } } 前端页面index.html代码。<!-- index.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <body> <video id="video" width="500px" height="500px" autoplay="autoplay"></video> <canvas id="canvas" width="500px" height="500px"></canvas> <br> <input type="button" title="HTML5摄像头" value="开启摄像头" onclick="getMedia()"/> <script>function getMedia() { let constraints = { video: {width: 500, height: 500}, audio: true }; // 获取video摄像头区域 let video = document.getElementById("video"); // 返回的Promise对象 let promise = navigator.mediaDevices.getUserMedia(constraints); // then()异步,调用MediaStream对象作为参数 promise.then(function (MediaStream) { video.srcObject = MediaStream; video.play(); }); } </script> </body> </html>
-
问题说明今天我们来讲讲微信开放平台的三方库,相信很多同学在开发的时候都会遇到各种各样的坑。原因分析:鸿蒙版本基于微信第三方库,分享文字、分享图片、分享网页、分享小程序、打开小程序、打开微信相信很多同学在开发的时候都会遇到各种各样的坑,比如各种配置,导入等这些问题解决思路:使用三方库@free/wechat,让大家开发更加便捷!解决方案:安装教程ohpm install @free/wechat使用说明注意:必须要保证AGC签名和微信上保持一致1、初始化,配置wxId以及handle监听方法WeChat.install.setConfig({wxId:“wxc4daXXXXXXXXXXX”,handleWant:want});2、基础分享配置参数(可选)interface HandlerAction{onResp?: (req: BaseResp) => void // 请求完成回调onReq?: (req: BaseReq) => void // 请求回调}interface MessageConfig extends HandlerAction{title?: string // 标题description?: string // 介绍thumb?: Resource | string //thumb64?: Resource | string // 图片太大需要压缩的thumb, 尽量自行处理图片.shareType?: number // 分享类型 会话: 0, 朋友圈1}3、分享文字WeChat.install.shareText(“text”,{onResp:(resp)=>{}})4、分享图片WeChat.install.shareImage(“https://img.ixintu.com/download/jpg/201911/e25b904bc42a74d7d77aed81e66d772c.jpg!con”,{onResp:(resp)=>{}})5、分享网页WeChat.install.shareWeb(“https://developer.huawei.com/consumer/cn/”,{onResp:(resp)=>{}})6、分享小程序WeChat.install.shareMini({username:“username”,path:“path”},{onResp:(resp)=>{}})7、打开微信WeChat.install.openWX(“weixin://dl/business/?t=YODfF8dNclo”,{onResp:(resp)=>{}})8、打开小程序WeChat.install.openMini({username:“username”,path:“path”},“path”,{onResp:(resp)=>{}})9、微信授权WeChat.install.sendAuth({scope:“snsapi_userinfo”,state:“session”},“path”,{onResp:(resp)=>{}})
-
一、问题说明RelativeContainer相对容器是我们日常开发中常用组件,尤其当我们开发复杂布局的时候,可以减少布局的层级,减少布局的渲染消耗。有些场景下我们需要让RelativeContainer自适应而不是写死高度,让如何让RelativeContainer自适应?二、原因分析鸿蒙的的相对布局自适应与安卓的自适应使用情况不同,按照安卓的使用方式给高度增加auto后不生效,我们查找官方文档发现了关键描述:什么意思?举个栗子,如果我们竖直方向上让高度自适应时,不仅设置高度auto同时还要要求子组件在竖直方向上没有父组件RelativeContainer锚点的Id引用、使用,通过代码我们观察效果。三、解决思路RelativeContainer作为容器高度auto,在竖直方向上有容器锚点的引用:RelativeContainer() { Text('我是子组件1') .width('100%') .height('100') .id('child_one') .textAlign(TextAlign.Center) .backgroundColor(Color.Pink) .alignRules({ left: { anchor: '__container__', align: HorizontalAlign.Start }, top: { anchor: '__container__', align: VerticalAlign.Top } }) Text('我是子组件2') .width('100%') .height('100') .id('child_two') .textAlign(TextAlign.Center) .backgroundColor(Color.Orange) .alignRules({ left: { anchor: '__container__', align: HorizontalAlign.Start }, top: { anchor: 'child_one', align: VerticalAlign.Bottom } }) Text('我是子组件3') .width('100%') .height('100') .id('child_three') .textAlign(TextAlign.Center) .backgroundColor(Color.Brown) .alignRules({ left: { anchor: '__container__', align: HorizontalAlign.Start }, top: { anchor: 'child_two', align: VerticalAlign.Bottom }, bottom: { anchor: '__container__', align: VerticalAlign.Bottom }, })}.height('auto').backgroundColor(Color.Blue)可以看到RelativeContainer的背景色仍然被拉满,竖直方向上铺满屏幕,auoto不生效。 RelativeContainer作为容器高度auto,在竖直方向上无容器锚点的引用: RelativeContainer() { Text('我是子组件1') .width('100%') .height('100') .id('child_one') .textAlign(TextAlign.Center) .backgroundColor(Color.Pink) .alignRules({ left: { anchor: '__container__', align: HorizontalAlign.Start } }) Text('我是子组件2') .width('100%') .height('100') .id('child_two') .textAlign(TextAlign.Center) .backgroundColor(Color.Orange) .alignRules({ left: { anchor: '__container__', align: HorizontalAlign.Start }, top: { anchor: 'child_one', align: VerticalAlign.Bottom } }) Text('我是子组件3') .width('100%') .height('100') .id('child_three') .textAlign(TextAlign.Center) .backgroundColor(Color.Brown) .alignRules({ left: { anchor: '__container__', align: HorizontalAlign.Start }, top: { anchor: 'child_two', align: VerticalAlign.Bottom } })}.height('auto').backgroundColor(Color.Blue)我们再通过ArkUI观察下:我们通过观察选中的RelativeContainer容器底部边界与子组件3的底部边界重合。四、解决方案水平方向上使用同理,我们结合当前布局的复杂程度灵活选用对应组件,减少布局的嵌套。
-
一、问题说明当手势在手机桌面上滑调起最近使用的应用,找到当前应用上滑关闭、进程杀死的时候,实况窗不跟随app一起消失、关闭二、原因分析当前封装的Component组件的生命周期的aboutToDisappear方法内有调用liveViewManager.stopLiveView(defaultView)实况窗的销毁方法,增加打点日志分析:日志1console.log(TAG,"aboutToDisappear") //在组件的aboutToDisappear增加日志日志2console.log(TAG, ` 清除当前实况窗 `) //在实况窗liveViewManager.stopLiveView()销毁的执行前打印日志通过日志发现日志1有打印,日志2无打印,也就是说组件的生命周期aboutToDisappear执行了,但是销毁实况窗的方法没有执行到三、解决思路分析到了原因,尝试怎么销毁,在何时销毁,页面内销毁还是Ability内销毁。 UIAbility-->onDestroy():在UIAbility实例销毁之前,系统触发onDestroy回调。该回调是UIAbility接收到的最后一个生命周期回调,开发者可以在onDestroy()回调中进行系统资源的释放、数据的保存等操作。 @Entry装饰的组件(页面)-->onPageShow:页面每次显示时触发一次,包括路由过程、应用进入前台等场景。onPageHide:页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。onBackPress:当用户点击返回按钮时触发。 Component组件-->aboutToDisappear:在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。四、解决方案实况窗是系统级功能,属于 HarmonyOS NEXT 系统的一部分,主要用于提升多任务处理效率和信息展示方式,,我们尝试在UIAbility-->onDestroy()生命周期内销毁。通过封装管理类,根据创建实况窗时传标识Id,建立Id与实况窗的映射关系,通过Id和该管理类建立的映射关系支持某个实况窗的关闭,也支持所有实况窗的关闭,当应用关闭的时候我们尝试在UIAbility-->onDestroy()生命周期内销毁所有实况窗。 经过测试验证、日志打印发现,对应的销毁日志执行到了,即实况窗销毁了,至此问题解决了。
-
1.问题说明: 需求:arkts通过native接口传递图片路径,并由native层读取图片获取图片参数和内容。 问题:打开图片文件失败,图片色彩偏差。2.原因分析: 打开图片文件失败的可能原因: (1)权限不足,系统限制打开该文件 (2)路径错误或文件异常 图片色彩偏差: (1)图片解码参数错误 (2)像素格式不匹配,需要转换3.解决思路: (1)检查文件所在目录是否有访问权限,访问权限是否申请。 (2)打印传递给native层的路径参数并检查,测试atkts打开图片是否异常。 (3)访问官方论坛查询相关接口,验证接口调用是否正确。 (4)仔细阅读文档或示例并打印相关数据做验证。4.解决方案: 打开图片文件失败的原因:传递的是沙箱路径,应转换为实际存储路径。相关转换代码如下: size_t argc = 1; napi_value argv[1] = {nullptr}; napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); std::unique_ptr < char[] > buf = std::make_unique < char[] >(2048); if (buf.get() == nullptr) { OH_LOG_ERROR(LOG_APP, "%s nullptr js object to string malloc failed.", __func__); return nullptr; } (void)memset(buf.get(), 0, 2048); size_t result = 0; napi_get_value_string_utf8(env, argv[0], buf.get(), 2048, &result); std::string target = buf.get(); unsigned int length = target.size(); char *pathResult = NULL; if(!target.empty() && strstr(target.c_str(), "file://")) { bool falgs = OH_FileUri_IsValidUri(target.c_str(), length); OH_LOG_INFO(LOG_APP, "The URI is valid? falgs=%{public}d", falgs); FileManagement_ErrCode err = OH_FileUri_GetPathFromUri(target.c_str(), length, &pathResult); if (err == 0 && pathResult != NULL) { OH_LOG_INFO(LOG_APP, "pathResult: %s", pathResult); } } 图片色彩偏差:像素格式不匹配,需要转换。图片解码及转换代码如下:typedef struct ImageInfo ImageInfo; struct ImageInfo { uint8_t *imageArray; uint32_t width; uint32_t height; }; ImageInfo createImageInfo(char* uri) { uint32_t width; uint32_t height; uint8_t *data = readImage(uri, &width, &height); if (!data) { return ImageInfo(); } ImageInfo info; info.imageArray = new (std::nothrow) uint8_t[width * height * 4]; memcpy(info.imageArray, data, width * height * 4); info.width = width; info.height = height; return info; }uint8_t* readImage(char *uri, uint32_t *width, uint32_t *height) { //创建ImageSource实例 OH_ImageSourceNative *source = nullptr; Image_ErrorCode errCode = OH_ImageSourceNative_CreateFromUri(uri, 2048, &source); if (errCode != IMAGE_SUCCESS) { //OH_LOG_ERROR(LOG_APP, "ImageSourceNativeCTest sourceTest OH_ImageSourceNative_CreateFromUri failed, errCode: %{public}d.", errCode); return nullptr; } //创建定义图片信息的结构体对象,并获取图片信息 OH_ImageSource_Info *imageInfo; OH_ImageSourceInfo_Create(&imageInfo); errCode = OH_ImageSourceNative_GetImageInfo(source, 0, imageInfo); if (errCode != IMAGE_SUCCESS) { //OH_LOG_ERROR(LOG_APP, "ImageSourceNativeCTest sourceTest OH_ImageSourceNative_GetImageInfo failed, errCode: %{public}d.", errCode); return nullptr; } //获取指定属性键的值 OH_ImageSourceInfo_GetWidth(imageInfo, width); OH_ImageSourceInfo_GetHeight(imageInfo, height); OH_ImageSourceInfo_Release(imageInfo); //通过图片解码参数创建PixelMap对象 OH_DecodingOptions *ops = nullptr; OH_DecodingOptions_Create(&ops); //设置为AUTO会根据图片资源格式解码,如果图片资源为HDR资源则会解码为HDR的pixelmap。 //OH_DecodingOptions_SetDesiredDynamicRange(ops, IMAGE_DYNAMIC_RANGE_AUTO); OH_PixelmapNative *resPixMap = nullptr; //ops参数支持传入nullptr, 当不需要设置解码参数时,不用创建 errCode = OH_ImageSourceNative_CreatePixelmap(source, ops, &resPixMap); OH_DecodingOptions_Release(ops); if (errCode != IMAGE_SUCCESS) { //OH_LOG_ERROR(LOG_APP, "ImageSourceNativeCTest sourceTest OH_ImageSourceNative_CreatePixelmap failed, errCode: %{public}d.", errCode); return nullptr; } uint8_t destination[(*width) * (*height) * 4]; size_t destinationSize = (*width) * (*height) * 4; errCode = OH_PixelmapNative_ReadPixels(resPixMap, destination, &destinationSize); if (errCode != IMAGE_SUCCESS) { //OH_LOG_ERROR(LOG_APP, "ImagePixelmapNativeCTest pixelmapTest OH_PixelmapNative_ReadPixels failed, errCode: %{public}d.", errCode); return nullptr; } bgra2rgba(destination, *width, *height); //memcpy(data, destination, destinationSize); //释放ImageSource实例 OH_ImageSourceNative_Release(source); //OH_LOG_INFO(LOG_APP, "ImagePixelmapNativeCTest pixelmapTest GetImageInfo success, width: %{public}d, height: %{public}d, rowStride: %{public}d, pixelFormat: %{public}d, alphaType: %{public}d.", width, height, rowStride, pixelFormat, alphaType); OH_PixelmapNative_Release(resPixMap); return destination; }void bgra2rgba(unsigned char *buffer, int width, int height) { unsigned int *rgba = (unsigned int *)buffer; for (int i = 0; i < width * height; i++) { rgba[i] = (rgba[i] & 0xFF000000) | // ______AA ((rgba[i] & 0x00FF0000) >> 16) | // RR______ (rgba[i] & 0x0000FF00) | // __GG____ ((rgba[i] & 0x000000FF) << 16); // ____BB__ } }
-
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%,全面提升用户操作的直观性与便捷性,最终实现时间段选择体验的优化升级。
-
1. 关键技术难点总结目前鸿蒙官方系统自带api:sendKeyEvent、deleteText无法直接删除自定义表情如下方所示4~9。1.1 问题说明 司内小伙伴在自定义键盘时发现4~9无法直接删除,需要点击多次才可,同时如维护整个输入事件成本太高。同时ai 查找解决方案存在兼容问题。系统自带的复合表情需要删除多次。1.2 原因分析 此处表情4~9不是常用表情,或者有可能是后续出现的表情,系统未做匹配。 查看现有ai 实现过于负杂,仅判断部分场景。1.3 解决思路 确认表情规则,例如表情长度、字符集编码构成等。 为了实现更好的兼容性,除问题表情符,其余调用系统api实现。 在实现过程中需要考虑用户可能切换光标位置,需要注意。 选择删除也需要兼容,例如用户选择多个表情符或文字。1.4 解决方案增加判断方法:isBadEmoji(s: string): boolean { return /[0-9]\u20E3\uFE0F/u.test(s);}监听输入框选择事件:.onTextSelectionChange((start, end) => { this.selectStart = start this.selectEnd = end})删除事件:customDelete(){ // 获取光标位置 const caret = this.controller.getCaretOffset().index; // 判断是否需要检测表情,主要依赖于光标起始和结束位置,目前出问题表情长度均为 3 const checkEmoji = (this.selectStart - this.selectEnd == 3 || this.selectStart - this.selectEnd == 0) && this.inputValue.length >= 3; // 检测是否需要兼容处理 if (checkEmoji && this.isValidEmoji(this.inputValue.slice(caret - 3, caret))) { this.controller.deleteText({ start: caret - 3, end: caret }) } else { sendKeyEvent({ type: KeyType.Down, keyCode: 2055, keyText: '', keySource: KeySource.Keyboard, deviceId: 0, metaKey: 0, timestamp: 0, stopPropagation: () => { }, intentionCode: IntentionCode.INTENTION_DOWN }) }}1.5 方案成果总结 开发层面:尽可能调用系统api,降低维护成本,自定义方法仅处理不兼容部分,能更好的适配多场 景下的兼容性,旨在补齐系统缺失部分。用户体验层面:补齐用户交互过程中,操作不连贯的困惑。经验总结:不要被一开始的问题复杂度所欺骗,制定计划,拆解问题,一层层逐步分析,先实现功 能,后续优化补足不足之处。 目前测试下来没得问题。如有小伙伴发现问题,欢迎指正。万分感谢~
-
1.问题说明:在 0.72.5-ohos-5.0-release 分支,docs\Samples\Sample\SampleProject\MainProject 下,执行 npm i 安装完各种库之后再执行 npm run dev:all ;进行打包发现打包失败,报错为:SampleProject@0.0.1 dev:allnpm run dev:basic && npm run dev:homepage && npm run dev:goods && npm run dev:details && npm run dev:test && npm run dev:sandboxSampleProject@0.0.1 dev:basicreact-native bundle-harmony --dev false --entry-file ./src/bundles/basic/basic.js --bundle-output ../../SampleApp/entry/src/main/resources/rawfile/bundle/basic/basic.harmony.bundle --config ./basic.config.jserror: unknown command 'bundle-harmony' 2.原因分析:分析问题发生的原因cli已经安装,且bundle-harmony.js也存在,那应该不是cli的问题那应该就是bundle-harmony的问题3.解决思路:问题解决的思考过程打开package.json查找bundle-harmony用到了哪些库,发现最新的 mefs和比这里需要的用新很多 4.解决方案:描述实施的核心解决方案在主项目的package.json加入 : "memfs": "4.17.2",
推荐直播
-
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步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签