-
# 问题说明仓颉编程语言是一款面向全场景智能的新一代编程语言,主打原生智能化、天生全场景、高性能、强安全。主要应用于鸿蒙原生应用及服务应用等场景中,为开发者提供良好的编程体验。## 初识仓颉(1)> 编译器安装在使用仓颉编程语言进行开发时,可以选择DevEco Studio或者VsCode等主流软件进行开发,由于本篇文章主要介绍使用仓颉编程语言进行鸿蒙原生应用的开发,故不再做过多介绍。感兴趣的小伙伴可以参考仓颉编程语言官网进行学习。使用DevEcoStudio和仓颉编程语言进行开发鸿蒙原生应用,需要在开发者官网上申请开发者账号,并且通过审核后,才可以获取到对应的资源包。> 安装DevEco Studio过程不再进行演示,如果有需要的同学可以通过开发者官网进行查看通过审核后,就可以在下载资源中看到对应的插件,如图:下载完成后,只需在DevEco Studio中安装即可使用:Step1:下载后无需解压Step2:选择从磁盘中加载插件Step3:创建一个新项目Step4:将项目运行后,即可看到屏幕显示“Hello Cangjie”字样本期内容就先介绍这么多,如有纰漏还请指正,谢谢
-
问题说明在开发一个电商类鸿蒙应用时,我们遇到了一个典型的性能瓶颈:商品列表页在快速滑动时会出现明显的卡顿、掉帧现象。具体现象描述:用户快速上下滑动浏览商品列表时,屏幕内容无法流畅跟随手指滚动。出现白块、跳帧,视觉上感到“一顿一顿”的。在低端设备上,此问题尤为严重,甚至伴有短暂的假死现象。通过 DevEco Studio 的 Performance Profiler 工具抓取分析,可以观察到UI线程(主线程)存在大量的耗时操作和帧超时(Frame overdue)。这个问题严重影响了用户的浏览体验,是必须解决的高优先级生产问题。原因分析通过对问题页面的代码进行排查和性能剖析,我们归纳出以下几个主要原因:UI 布局过于复杂:单个列表项(ListItem)的组件树层级过深,包含了大量的嵌套容器和组件。测量(measure)、布局(layout)和绘制(draw)每一个复杂的项都需要消耗大量计算资源。主线程耗时操作:在 @Builder 或组件 build 函数中,同步执行了繁重的数据处理或图片解码操作。ArkUI 的UI更新是单线程模型,这些操作会阻塞UI线程,导致无法在16ms内完成一帧的绘制(以达到60FPS的流畅度)。图片加载不当:直接使用巨大的图片资源而未进行适当缩放,或者在滑动过程中未对图片加载进行优化,导致在滑动时频繁进行IO操作和解码,占用大量CPU资源。无效的重复渲染:由于状态管理不当,列表在滑动时触发了不必要的全局刷新或过多项的重新构建。解决思路解决列表卡顿的核心思路是:减轻主线程负担,减少每帧的计算量。扁平化布局:简化单个列表项的UI结构,减少嵌套层级,使用更高效的布局组件。异步与懒加载:将耗时操作(如数据加工、图片解码)移出UI线程,放到异步任务或Worker中处理。对于图片,采用懒加载策略,仅在需要显示时才进行加载。复用与缓存:充分利用 List 组件的原生复用机制(回收不可见项的视图,复用其结构用于新出现的项)。同时,对已加载的图片、计算结果等进行缓存,避免重复计算。精细化渲染控制:使用 @ObjectLink 和 @Observed 来实现组件级别的状态更新,避免因某个列表项的数据变化而导致整个列表的重新渲染。解决方案使用精准渲染控制(ArkUI最佳实践)对于复杂列表项,避免使用 @State 管理整个数组。使用 @Observed 和 @ObjectLink 来实现项级别的更新。// 定义可观察的类@Observedclass GoodsInfo {id: string;name: string;price: number;imageUrl: string;// …}@Componentstruct GoodsListItem {@ObjectLink item: GoodsInfo; // 关联到可观察对象的某个属性build() {Row() {Image(this.item.imageUrl)Text(this.item.name)Text(¥${this.item.price})}// 当这个具体的 item 发生变化时,只会重新构建这个 GoodsListItem 组件,// 而不是整个List,极大提升性能。}}// 在Page中使用@Entry@Componentstruct GoodsPage {@State goodsList: GoodsInfo[] = []; // List管理数组引用build() {List() {ForEach(this.goodsList, (item: GoodsInfo) => {ListItem() {GoodsListItem({ item: item }) // 将每个项传递给子组件}}, (item: GoodsInfo) => item.id)}}}
-
在万物互联时代,用户需求正从“人找服务”逐步向“服务找人”转变。HarmonyOS 以用户为中心,依托POI、信标、鸿蒙标签、NFC iTAP等技术打造近场服务能力,将近场服务融入用户日常生活场景,悄然改变众多领域的服务体验。本期近场服务聚焦商超、文旅、餐饮三大行业的典型应用场景,带你感受HarmonyOS近场服务带来的体验提升。一、智慧商超:为商铺装上“智能导购” 在传统商超综合体中,商铺客流大多依赖品牌影响力和区位优势,普通商铺难以有效吸引顾客驻足。 而当商铺部署信标设备后,用户进入信标连接范围即可收到传输信号,通过“小艺建议”获取门店活动、特色服务等推荐,助力商家在用户消费决策前实现精准曝光,显著提升店铺引流能力,为会员转化和成交率带来新增长点。 二、智慧文旅:打造沉浸式游览体验 假期出游高峰时,排队购票导致入园拥堵、景区导览设置不清导致错过打卡点等都会影响游客的游览体验。 近场服务基于POI位置推荐可在游客靠近景区附近时通过小艺建议获取购票服务卡片推荐,一键直达购票页面,比传统线上购票软件减少约50%操作步骤。进入景区游览时,游客也可以基于景区内不同景点的POI点位推荐一键跳转至景区元服务详情页,当前景点讲解、后续景点推荐、游览路线推荐等一目了然,告别盲目寻找和人工问询。 三、智慧餐饮:一碰直达,极速点餐 餐饮门店可在餐桌或入口处设置HarmonyOS标签,用户通过手机“碰一碰”即可快速直达商家元服务页面。 消费者无需排队点单,手机“碰一碰”即可实现会员一键入会、获取优惠套餐、快速点餐等。不仅大大缩短用户操作步骤,提升了用户体验,也帮助商家大幅提升会员转化与订单效率,实现用户与商家的双赢。 HarmonyOS近场服务在以上行业应用场景中展示了强大的适配性和创新价值。除上述典型案例场景之外,还广泛应用在智慧办公、运动健康、本地生活、政务民生等领域。欢迎开发者点击下方链接了解并接入使用,与HarmonyOS一起共建共享鸿蒙新世界! 👉 点击了解更多并申请接入:申请开通权限-近场服务 - 华为HarmonyOS开发者 (huawei.com) AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给你带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢你对HUAWEI AppGallery Connect的支持!
-
对H5页面占比高的APP而言,“加载慢”是用户体验的“头号杀手”——转圈的加载动画、迟迟不显示的内容,很容易让用户直接退出。为解决这一痛点,AppGallery Connect推出高性能Web容器组件FastWeb,专为H5页面提速而生,帮开发者搞定H5优化,让用户告别“加载卡顿”烦恼,体验更丝滑。一、先搞懂:什么是FastWeb组件?FastWeb是基于OpenHarmony开发的“高性能Web容器”,适用于对H5页面有性能优化需求(加载提速)的场景。像电商APP的商品详情页、资讯新闻列表页、工具类功能操作页等,只要是以H5形式呈现且对页面性能优化有诉求,希望提升加载速度,FastWeb都能派上用场。它聚焦网络大资源的“提速”核心,而非复杂业务逻辑的处理,旨在帮助大家用轻量化开发实现加载优化。二、两种使用方式:按需选择,灵活配置考虑到不同APP的H5开发现状,FastWeb提供两种灵活方案,无论全面改造还是增量式“迭代开发”,都带来了不错的提升效果。实验数据显示,某APP首次打开且无缓存时,直接加载Web页面需5413.58ms,多次打开有缓存时仍需1345.93ms,这是因为该方式要在页面加载时才拉起渲染进程、发起资源请求,额外增加了加载耗时;而使用FastWeb组件后,首次打开(无缓存)加载页面加载时间缩短49.9%;多次打开(有缓存)页面加载时间缩短39.7%。具体数据如下: 方式一:全面改造,解锁全能力若想彻底发挥FastWeb的优化实力,即便H5已封装过Web容器,也能通过此方式“全方位提速”。它会调用预启动、预渲染、预编译JavaScript生成字节码缓存、离线资源拦截注入四大能力,从“提前准备”到“资源复用”拉满效率。操作很简单:APP启动时(或合适时机)创建空的ArkWeb组件“预热”,展示H5页面时直接挂载即可。需注意删除原有Web容器,将属性和事件写入FastWeb暴露对象,适合有调整空间的团队。方式二:增量式“迭代开发”,快速提效如果已经将H5页面封装成Web容器,并希望在不修改原页面的基础上进行优化,你可以通过FastWeb的预编译JavaScript生成字节码缓存、离线资源拦截注入两大能力,实现提速。操作逻辑同上:提前创建空ArkWeb组件,可以在App启动时创建,或者其他合适的页面创建。展示H5时直接用原有页面,无需额外调整。适合追求“低成本快速优化”的团队,兼顾效果与业务稳定性。三、实用建议:避坑指南,用得更顺手想让FastWeb稳定发挥提速效果,这几个细节要注意:FastWeb组件的核心优势在于网络大资源的预加载能力,而非复杂业务逻辑处理,建议优先用于首页H5、高频核心页等“优化关键路径”,能让提速效果更突出。若应用涉及桥接功能需求,优先选方式二,避免改动原有容器,确保通信稳定的同时,不影响加载速度提升。创建FastWeb组件将占用内存(每个FastWeb组件大约200MB)和计算资源,建议避免一次性创建大量FastWeb组件,按页面访问频率合理规划,避免出现“为了快而牺牲流畅”的情况。对H5多的APP来说,FastWeb不是“可选优化项”,而是“刚需组件”。它无需复杂适配,两种方式覆盖不同开发场景。若你正为H5加载慢头疼,不妨试试FastWeb——让用户告别等待,让APP体验再上台阶。AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给你带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢你对HUAWEI AppGallery Connect的支持!
-
在用户注意力稀缺的今天,如何让每一次触达都精准转化为应用内的活跃行为?华为AppGallery Connect(简称AGC)向开发者推出App Linking技术服务,提供“应用链接”和“元服务链接”,可直接跳转HarmonyOS应用或者跳转元服务,有效简化用户访问路径。无论是内容分享、游戏互动还是服务直达,App Linking都能提供有力支持。它不仅能帮助开发者提升应用的竞争力,还能为用户带来更便捷、高效的使用体验。 今天就来盘点下,App Linking 到底有哪些好用的全场景链接技巧!一、社交互动篇:2个技巧解锁社交分享新玩法 社交分享是用户传播的核心场景,但传统分享常因 “操作复杂、跳转卡顿” 流失用户。App Linking 通过2个技巧,让社交分享既有趣又高效,轻松提升裂变转化效果。App Linking+华为分享,助力线上社交裂变 核心功能:依托 HarmonyOS 系统级分享面板,支持直接生成带应用 / 元服务入口的分享链接,可无缝分享至微信、畅联等主流社交 AppApp Linking+碰一碰分享,社交分享新体验 核心功能:两部设备轻轻一碰即可传递链接,实现 “一碰即传、极简操作”,带来全新的社交互动体验,趣味性与便捷性兼顾。 点击查看场景案例: 华为视频碰一碰,让跨设备视频分享一步到位 二、服务触达篇:3 个方案助力服务直达 App Linking 通过3种针对性方案,实现无需提前打开 App,没有复杂跳转过程,就可直达服务。App Linking+系统扫码,一扫直达目标页面 核心功能:多渠道扫码,负一屏、控制中心、系统相机均可通过扫码,无需用户打开App,通过系统扫码直达应用的核心页面。App Linking+智能消息,一步直达服务页面 核心功能:智能消息作为营销活动的优秀载体。从消息一键直达服务,体验友好。可以提高营销转化率。App Linking+鸿蒙标签,服务一碰即达 核心功能:即碰即走,方便快捷;碰扫合一,多样化体验。便捷使用,需要碰一碰服务标签即可获取服务信息。 点击查看场景案例:美团一扫即达,服务快人一步,操作效率提升30%以上 三、进阶攻略篇:2 个工具让分享链路精准触达直达应用市场:目标应用 “点击即达”,减少流量流失 核心功能:当成功配置App Linking应用链接后,可以构建App Linking直达链接。当应用已安装时,点击链接直接跳转应用;当应用未安装时,点击链接跳转应用市场下载详情页,引导用户下载应用。延迟链接:跳转 “不跑偏”,提升转化效率 核心功能:当被分享用户未安装应用时,通过延迟链接能力,应用首次打开时,系统仍能获取用户之前点击的应用相关链接。在获取链接后,应用可直接跳转至对应的详情页,无需先跳转至应用首页,从而提升用户体验和链接的转化率。 点击查看场景案例: App Linking助力华为阅读分享链路精准触达,操作步骤减43%! 对于开发者而言,App Linking 不只是简单的链接工具,更是提升用户使用体验的核心利器。它打通 “用户触达” 与 “服务落地”,让应用与用户连接更高效。点击下方链接,即刻开启鸿蒙生态场景化运营新篇章 ——App Linking 。 AppGallery Connect致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。为给你带来更好服务,请扫描下方二维码或者点击此处免费咨询。 如有任何疑问,请发送邮件至agconnect@huawei.com咨询,感谢你对HUAWEI AppGallery Connect的支持!
-
一、问题说明**屏幕方向适配问题:**横竖屏切换时布局样式未自适应调整**全屏控制API使用问题:**getContext() 已废弃导致全屏控制失效**动画控制机制优化:**定时器与手动控制存在状态冲突风险二、原因分析MediaQueryListener 监听逻辑未正确绑定生命周期横屏判断条件 min-aspect-ratio: 1.5 可能不适用于所有设备样式调整逻辑分散在布局代码中,可维护性差旧版 getContext() 方法已被鸿蒙新架构弃用未正确处理窗口获取的异步操作全屏状态切换未考虑生命周期管理定时器未正确清理可能造成内存泄漏全屏状态与动画状态未解耦缺乏平滑的动画过渡效果三、解决思路使用 @ohos.mediaquery 标准API进行屏幕方向监听将布局参数抽离为状态变量集中管理通过 @Builder 分离不同屏幕方向的布局实现使用 getUIContext() 替代弃用API采用 @WindowLink 实现窗口状态同步增加错误边界处理使用 TransitionEffect 实现组合动画通过 Animator 管理动画生命周期采用Promise链式调用确保状态顺序四、解决方案屏幕方向适配问题优化:@Component export struct TransitionsAnimation { @State private isLandscape: boolean = false private mediaListener: mediaquery.MediaQueryListener = mediaquery.matchMedia('(orientation: landscape)') aboutToAppear() { this.mediaListener.on('change', (result: mediaquery.MediaQueryResult) => { this.isLandscape = result.matches }) } @Builder DynamicMargin(topValue: number | string) { Column() .margin({ top: this.isLandscape ? '10vp' : `${topValue}vp` }) } } 全屏控制API使用问题优化:export class WindowUtils { static async setFullScreen(enabled: boolean) { try { const window = await window.getLastWindow(this.getUIContext()) await window.setWindowLayoutFullScreen(enabled) await window.setWindowSystemBarEnable([]) // 隐藏状态栏 } catch (err) { console.error('Window control failed:', err.message) } } } 动画控制机制优化:@Component struct TransitionEffectExample { @State isVisible: boolean = true build() { Column() { if (this.isVisible) { Image($r('app.media.logo')) .transition(TransitionEffect.OPACITY .combine(TransitionEffect.scale({ x: 0, y: 0 })) .combine(TransitionEffect.rotate({ angle: 180 })) ) } Button('切换') .onClick(() => { this.isVisible = !this.isVisible WindowUtils.setFullScreen(!this.isVisible) }) } .animation({ duration: 1000, curve: Curve.EaseInOut }) } } 工程化建议:组件拆分:将动画元素封装为 @Reusable 组件状态管理:使用 @Provide/@Consume 实现跨组件状态共享性能优化:.onAppear(() => { this.mediaListener = mediaquery.matchMedia(...) }) .onDisappear(() => { this.mediaListener.off('change') }) 设备适配:在 resource 目录下定义多维度资源文件
-
1. 问题说明在实现WebRTC视频通信系统时,开发者面临的主要问题包括:信令传输问题:WebRTC本身不提供信令传输机制,需要自行实现客户端之间的通信协调NAT穿透难题:在不同网络环境下的设备之间建立直接P2P连接存在技术障碍媒体流处理复杂:音视频流的采集、编码、传输和渲染需要处理多种技术细节跨浏览器兼容性:不同浏览器对WebRTC的支持程度和API实现存在差异错误处理机制不足:网络异常、设备权限等问题需要完善的错误处理和重试机制2. 原因分析通过对WebRTC通信流程的深入分析,可以识别出以下关键原因:2.1 信令机制缺失WebRTC标准未规定信令协议,需要开发者自行选择实现方式信令服务器需要处理SDP交换和ICE候选信息传递2.2 NAT和防火墙限制企业网络和家庭路由器通常使用NAT技术防火墙策略可能阻止P2P直接连接STUN/TURN服务器配置复杂2.3 媒体流处理复杂性音视频编解码器协商需要精确匹配网络状况变化需要动态调整码率和分辨率设备权限获取需要用户明确授权2.4 浏览器兼容性问题不同浏览器对WebRTC API的支持程度不同SDP格式和ICE候选处理存在细微差异2.5 状态管理困难连接建立过程涉及多个异步步骤错误处理和状态恢复机制复杂3. 解决思路基于以上分析,提出以下系统性的解决方案思路:3.1 分层架构设计信令层:使用WebSocket实现高效的双向通信媒体层:基于WebRTC实现音视频传输控制层:管理连接状态和错误处理3.2 完善的信令协议定义标准的消息格式和类型实现SDP交换和ICE候选传递机制添加心跳检测和重连机制3.3 健壮的NAT穿透策略集成STUN服务器进行地址发现备用TURN服务器中继方案动态ICE候选收集和处理3.4 兼容性处理浏览器特性检测和polyfill统一的API封装和错误处理降级方案和功能检测3.5 用户体验优化清晰的连接状态指示自动重连和恢复机制详细的错误信息和调试支持4. 解决方案4.1 增强型信令服务器实现const WebSocket = require('ws'); const http = require('http'); class SignalingServer { constructor(port = 8080) { this.port = port; this.wss = null; this.clients = new Map(); // 客户端管理 this.initializeServer(); } initializeServer() { const server = http.createServer(); this.wss = new WebSocket.Server({ server }); this.wss.on('connection', (ws, request) => { this.handleConnection(ws, request); }); server.listen(this.port, () => { console.log(`Signaling server running on ws://localhost:${this.port}`); }); } handleConnection(ws, request) { const clientId = this.generateClientId(); this.clients.set(clientId, { ws, id: clientId }); console.log(`Client ${clientId} connected`); // 发送欢迎消息和客户端ID ws.send(JSON.stringify({ type: 'welcome', clientId: clientId, timestamp: Date.now() })); ws.on('message', (message) => { this.handleMessage(clientId, message); }); ws.on('close', () => { this.handleDisconnection(clientId); }); ws.on('error', (error) => { this.handleError(clientId, error); }); } handleMessage(clientId, message) { try { const parsedMessage = JSON.parse(message.toString()); // 验证消息格式 if (!this.validateMessage(parsedMessage)) { this.sendError(clientId, 'Invalid message format'); return; } switch (parsedMessage.type) { case 'offer': case 'answer': case 'candidate': this.relayMessage(clientId, parsedMessage); break; case 'ping': this.handlePing(clientId); break; default: this.sendError(clientId, 'Unknown message type'); } } catch (error) { console.error('Message parsing error:', error); this.sendError(clientId, 'Message parsing failed'); } } relayMessage(senderId, message) { // 这里假设消息中包含目标客户端ID const targetClient = this.clients.get(message.targetClientId); if (targetClient && targetClient.ws.readyState === WebSocket.OPEN) { targetClient.ws.send(JSON.stringify({ ...message, senderClientId: senderId })); } } generateClientId() { return Math.random().toString(36).substring(2, 15); } validateMessage(message) { return message && message.type && typeof message.type === 'string'; } handlePing(clientId) { const client = this.clients.get(clientId); if (client) { client.ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); } } handleDisconnection(clientId) { this.clients.delete(clientId); console.log(`Client ${clientId} disconnected`); // 通知其他客户端 this.broadcast({ type: 'client-disconnected', clientId: clientId, timestamp: Date.now() }); } handleError(clientId, error) { console.error(`Client ${clientId} error:`, error); } sendError(clientId, errorMessage) { const client = this.clients.get(clientId); if (client) { client.ws.send(JSON.stringify({ type: 'error', message: errorMessage, timestamp: Date.now() })); } } broadcast(message) { const messageString = JSON.stringify(message); this.clients.forEach(client => { if (client.ws.readyState === WebSocket.OPEN) { client.ws.send(messageString); } }); } } // 启动服务器 new SignalingServer(8080); 4.2 增强型客户端实现<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>WebRTC视频通信系统</title> <style> .video-container { display: flex; gap: 20px; margin-bottom: 20px; } video { width: 400px; height: 300px; border: 1px solid #ccc; background-color: #000; } .controls { margin: 20px 0; } button { padding: 10px 20px; margin: 0 10px 10px 0; font-size: 16px; cursor: pointer; } textarea { width: 100%; height: 200px; margin-bottom: 20px; } .status { padding: 10px; margin: 10px 0; border: 1px solid #ccc; background-color: #f9f9f9; } </style> </head> <body> <h1>WebRTC视频通信系统</h1> <div class="video-container"> <div> <h3>本地视频</h3> <video id="localVideo" autoplay muted></video> </div> <div> <h3>远程视频</h3> <video id="remoteVideo" autoplay></video> </div> </div> <div class="status" id="connectionStatus">未连接</div> <div class="controls"> <button onclick="startCamera()">开启摄像头</button> <button onclick="createOffer()">创建连接</button> <button onclick="hangUp()">挂断</button> </div> <textarea id="messages" readonly></textarea> <script> class WebRTCClient { constructor() { this.socket = null; this.peerConnection = null; this.localStream = null; this.remoteStream = null; this.clientId = null; this.targetClientId = null; this.initialize(); } initialize() { this.initializeUI(); this.initializeWebSocket(); this.initializePeerConnection(); } initializeUI() { this.messages = document.getElementById('messages'); this.connectionStatus = document.getElementById('connectionStatus'); this.localVideo = document.getElementById('localVideo'); this.remoteVideo = document.getElementById('remoteVideo'); } initializeWebSocket() { const serverUrl = 'ws://10.88.27.199:8080'; this.socket = new WebSocket(serverUrl); this.socket.onopen = () => { this.log('连接到信令服务器'); this.updateStatus('已连接到信令服务器'); }; this.socket.onmessage = (event) => { this.handleSignalingMessage(event.data); }; this.socket.onclose = () => { this.log('信令服务器连接断开'); this.updateStatus('信令服务器连接断开'); }; this.socket.onerror = (error) => { this.log('WebSocket错误: ' + error); this.updateStatus('连接错误'); }; } initializePeerConnection() { const configuration = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' } ] }; this.peerConnection = new RTCPeerConnection(configuration); // 设置事件监听器 this.peerConnection.onicecandidate = (event) => { if (event.candidate) { this.sendSignalingMessage({ type: 'candidate', candidate: event.candidate, targetClientId: this.targetClientId }); } }; this.peerConnection.ontrack = (event) => { this.remoteStream = event.streams[0]; this.remoteVideo.srcObject = this.remoteStream; this.log('接收到远程视频流'); this.updateStatus('视频连接已建立'); }; this.peerConnection.onconnectionstatechange = () => { this.updateStatus('连接状态: ' + this.peerConnection.connectionState); }; this.peerConnection.oniceconnectionstatechange = () => { this.log('ICE连接状态: ' + this.peerConnection.iceConnectionState); }; } async startCamera() { try { const constraints = { video: { width: 640, height: 480 }, audio: true }; this.localStream = await navigator.mediaDevices.getUserMedia(constraints); this.localVideo.srcObject = this.localStream; // 添加本地流到peer connection this.localStream.getTracks().forEach(track => { this.peerConnection.addTrack(track, this.localStream); }); this.log('摄像头启动成功'); this.updateStatus('摄像头就绪'); } catch (error) { this.log('摄像头访问失败: ' + error.message); this.updateStatus('摄像头访问被拒绝'); } } async createOffer() { if (!this.localStream) { this.log('请先启动摄像头'); return; } try { const offer = await this.peerConnection.createOffer(); await this.peerConnection.setLocalDescription(offer); this.sendSignalingMessage({ type: 'offer', sdp: offer, targetClientId: this.targetClientId }); this.log('已创建并发送Offer'); this.updateStatus('等待对方应答'); } catch (error) { this.log('创建Offer失败: ' + error); } } async handleOffer(offer, senderClientId) { this.targetClientId = senderClientId; try { await this.peerConnection.setRemoteDescription(offer); const answer = await this.peerConnection.createAnswer(); await this.peerConnection.setLocalDescription(answer); this.sendSignalingMessage({ type: 'answer', sdp: answer, targetClientId: this.targetClientId }); this.log('已处理Offer并发送Answer'); } catch (error) { this.log('处理Offer失败: ' + error); } } async handleAnswer(answer) { try { await this.peerConnection.setRemoteDescription(answer); this.log('已处理Answer,连接建立中'); } catch (error) { this.log('处理Answer失败: ' + error); } } async handleCandidate(candidate) { try { await this.peerConnection.addIceCandidate(candidate); } catch (error) { this.log('添加ICE候选失败: ' + error); } } handleSignalingMessage(message) { try { const parsedMessage = JSON.parse(message); switch (parsedMessage.type) { case 'welcome': this.clientId = parsedMessage.clientId; this.log('连接到服务器,客户端ID: ' + this.clientId); break; case 'offer': this.handleOffer(parsedMessage.sdp, parsedMessage.senderClientId); break; case 'answer': this.handleAnswer(parsedMessage.sdp); break; case 'candidate': this.handleCandidate(parsedMessage.candidate); break; case 'error': this.log('服务器错误: ' + parsedMessage.message); break; default: this.log('未知消息类型: ' + parsedMessage.type); } } catch (error) { this.log('消息处理错误: ' + error); } } sendSignalingMessage(message) { if (this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(message)); } else { this.log('WebSocket未连接,无法发送消息'); } } hangUp() { if (this.peerConnection) { this.peerConnection.close(); this.initializePeerConnection(); } if (this.localStream) { this.localStream.getTracks().forEach(track => track.stop()); this.localStream = null; this.localVideo.srcObject = null; } this.remoteVideo.srcObject = null; this.log('通话已结束'); this.updateStatus('通话结束'); } log(message) { const timestamp = new Date().toLocaleTimeString(); this.messages.value += `[${timestamp}] ${message}\n`; this.messages.scrollTop = this.messages.scrollHeight; } updateStatus(status) { this.connectionStatus.textContent = status; } } // 全局客户端实例 let webrtcClient; // 页面加载完成后初始化 window.onload = function() { webrtcClient = new WebRTCClient(); }; // 全局函数供按钮调用 function startCamera() { webrtcClient.startCamera(); } function createOffer() { // 在实际应用中,这里应该通过UI选择目标客户端 webrtcClient.targetClientId = prompt('请输入目标客户端ID:'); if (webrtcClient.targetClientId) { webrtcClient.createOffer(); } } function hangUp() { webrtcClient.hangUp(); } </script> </body> </html>
-
一、问题说明直播场景下播控中心显示了已播放时间、视频时长、播放进度条,用户滑动播放进度条可以seek视频,此时的seek无任何意义。二、原因分析业务侧没有区分直播、点播等场景,播控中心会话接收播放器的进度后进行了更新操作,可以不用考虑业务如何实现、如何更新视频播放的positon、duration,我们关注播控会话调用更新API方法侧,这样改动最小,影响最小。三、解决思路1.查找官方文档,涉及播控中心更新数据视频相关的position、duration相关方法等,了解如何使用2.梳理本地播控中心管理类更新当前视频相关的position、duration相关方法3.梳理取消直播场景session下的相关监听四、解决方法1.根据官方API在setAVMetadata、setAVPlaybackState方法中调整position、duration2.按照1调整完后在投屏过程中仍然发现会播放时长显示,排查到投屏方法prepare(item: AVQueueItem): Promise<void>;会进行传值duration,将duration字段值赋值-1后,在播放视频过程中、或者投屏过程中,直播节目播放中心不在显示播放position、duration,播控中心上一个或者下一个节目验证也是没问题的。3.移除直播场景下的播控会话监听播放进度也不能seek至此,播控中心播放时间不显示、总时长不显示、进度条不能seek,完成业务侧需求。
-
一、关键技术难点总结1.问题说明在实际应用开发中,用户对于视频预览播放(如会话聊天中的视频消息播放、图片视频空间的视频预览等场景)是非常常见的需求。然而,鸿蒙原生的Video组件ui效果无法满足用户需求。Ui的播放暂停按钮需要自定义:Video组件只是单纯的加载播放的组件,播放暂停等常用功能按钮需要自己定义:开发人员在使用video的时候如果每次都需要去实现一套ui以及各种基础功能的api会导致整体效率不高且效果各异播放器的动画效果等统一封装后可以在后期需要改动产品效果等时,统一修改更加高效2.原因分析(1) 原生播放组件无法满足需求VideoPreview组件的核心定位是单一维度的视频预览播放工具,其设计初衷是满足用户预览视频的需求。这种定位决定了组件在功能规划上更侧重整体播放的效果以及ui的统一性,原生的video组件无法满足这个需求。(2) 开发逻辑的独立性VideoPreview组件的底层实现逻辑具有较强的独立性与封闭性。每个组件实例仅负责处理自身对应的视频数据(如本地视频数据以及网络视频数据)。(3) 开发冗余使用多个不同开发者开发的 video组件进行视频播放时,不同的开发者对于最终ui效果以及动画效果的理解差异,会导致最终呈现给用户的最终预览效果的差异,这样不仅开发人员各自增加了开发工作量,也无法很好的给用户提供统一、优质的视频预览效果,最终影响开发效率和使用体验。3.解决思路(1) 组件整合:打造统一标准的视频播放器组件针对鸿蒙原生 video组件没有统一样式的播放按钮的痛点(核心思路是基于组件化思想对原生组件进行封装扩展,通过复用原生能力、状态管理与动态适配,实现播放、暂停、重播、未加载完成时的预览图功能。具体包括:自定义video组件基础能力,组合播放、暂停、重播功能的统一ui按钮,播放进度条样式,解决ui标准不统一问题;采用鸿蒙装饰器实现视频数据必传入的方式,让开发人员很容易理解应该如何传值,可减少开发成本,提升开发效率;封装组件进度播放时、暂停时的动画,根据当前播放状态展示不同按钮(如播放中、播放完成、播放进度条平滑隐藏等)。(2) 交互增强:提升播放暂停完成时的动画效果播放的时候,用户点击可以显示或者隐藏播放进度条,同时平滑处理显示与隐藏动画,提高用户体验。4.解决方案(1) Ui实现:通过自定义按钮资源已经布局,封装VideoPreview组件。示例代码:@Observed export class VideoPreviewViewModel { // 视频控制器 controller: VideoController = new VideoController(); // 设置当前播放时间 setCurrentTime(time: number): void { this.controller.setCurrentTime(time) } } @Component export struct VideoPreview { // 视频源地址(必传) @Prop videoUri: Resource | string = '' // 预览图片地址 @Prop imgUri: Resource | string = '' // 是否自动播放 @Prop autoPlay: boolean = true // 播放速度 @Prop speed: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X // 关闭事件回调 onClose?: () => void // 组件内部状态 @State state: VideoState = new VideoState() @State animationProperty: AnimationOption = new AnimationOption() // 视图模型 @State viewModel: VideoPreviewViewModel = new VideoPreviewViewModel() aboutToAppear() { this.animationProperty.duration = 300 this.animationProperty.curve = Curve.EaseInOut } build() { Stack() { this.VideoBuilder() this.buildControls() // 加载状态显示 if (this.state.isLoading) { Image(this.imgUri) .width('100%') .height('100%') .objectFit(ImageFit.Cover) LoadingProgress() .width(40) .height(40) .color(Color.White) } } .width('100%') .height('100%') .backgroundColor(Color.Black) } // 视频播放器构建器 @Builder VideoBuilder() { Stack() { // 重播按钮(播放完成时显示) if (this.state.isFinish) { Column() { Image($r('app.media.replay_video')) .width(50) .height(50) .onClick(() => { this.viewModel.controller.start() }) } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) .alignItems(HorizontalAlign.Center) .zIndex(33) } // 视频组件 Video({ controller: this.viewModel.controller, currentProgressRate: this.state.speed, src: this.videoUri }) .muted(this.state.isVoiceOff) .objectFit(ImageFit.Contain) .autoPlay(this.autoPlay) .controls(false) .width('100%') .height('100%') .backgroundColor(Color.Black) .onPrepared((event: PreparedInfo) => { this.state.duration = event.duration this.state.isControlsVisible = 1 this.state.isLoading = false console.info('Video prepared, duration: ' + event.duration) }) .onUpdate((event: PlaybackInfo) => { this.state.currentTime = event.time }) .onStop(() => { this.state.isPlaying = false }) .onPause(() => { this.state.isPlaying = false }) .onStart(() => { this.state.isPlaying = true this.state.isLoading = false this.state.isFinish = false }) .onFinish(() => { this.state.isPlaying = false this.state.isFinish = true this.state.isLoading = false }) .onError(() => { console.error('Video playback error') this.state.isLoading = false }) } } // 控制栏构建器 @Builder buildControls() { Column() { // 顶部关闭按钮区域 Column() { Image($r("app.media.close_video")) .width(30) .height(30) .onClick(() => { if (this.onClose) { this.onClose() } }) } .width('100%') .height(80) .backgroundColor('#99000000') .padding({ top: 20, right: 12 }) .alignItems(HorizontalAlign.End) .visibility(this.state.isControlsVisible? Visibility.Visible : Visibility.Hidden) .animation(this.animationProperty) Blank() // 音量控制 Column() { Image(this.state.isVoiceOff ? $r('app.media.voice_off') : $r('app.media.voice_on')) .width(24) .height(24) .onClick(() => { this.state.isVoiceOff = !this.state.isVoiceOff }) } .padding({ right: 12 }) .width('100%') .visibility(this.state.isControlsVisible? Visibility.Visible : Visibility.Hidden) .alignItems(HorizontalAlign.End) // 底部进度控制区域 Column() { Row({ space: 8 }) { // 播放/暂停按钮 Image(this.state.isPlaying ? $r('app.media.pause_video') : $r('app.media.play_video')) .width(24) .height(24) .onClick(() => { if (this.state.isPlaying) { this.viewModel.controller.pause() } else { this.viewModel.controller.start() } this.state.isPlaying = !this.state.isPlaying }) .margin({ left: 10 }) // 当前时间 Text(this.formatTime(this.state.currentTime)) .fontColor(Color.White) .fontSize(12) .width(40) .textAlign(TextAlign.Center) // 进度条 Slider({ value: this.state.currentTime, min: 0, max: this.state.duration, style: SliderStyle.OutSet }) .layoutWeight(1) .blockColor(Color.White) .selectedColor('#FF4081') .trackColor('#CCCCCC') .trackThickness(3) .onChange((value: number) => { this.viewModel.controller.setCurrentTime(value) }) // 总时长 Text(this.formatTime(this.state.duration)) .fontColor(Color.White) .fontSize(12) .width(40) .textAlign(TextAlign.Center) .margin({ right: 10 }) } .width('100%') .height(60) .alignItems(VerticalAlign.Center) .backgroundColor('#99000000') } .width('100%') .visibility(this.state.isControlsVisible? Visibility.Visible : Visibility.Hidden) .animation(this.animationProperty) } .width('100%') .height('100%') // 手势控制:双击播放/暂停,单击显示/隐藏控制栏 .gesture( GestureGroup( GestureMode.Exclusive, TapGesture({ count: 2 }) .onAction(() => { if (this.state.isPlaying) { this.viewModel.controller.pause() } else { this.viewModel.controller.start() } this.state.isPlaying = !this.state.isPlaying }), TapGesture({ count: 1 }) .onAction(() => { if (this.state.isControlsVisible) { this.state.isControlsVisible = 0; } else { this.state.isControlsVisible = 1; } }) ) ) } // 时间格式化工具方法 private formatTime(seconds: number): string { const mins = Math.floor(seconds / 60) const secs = Math.floor(seconds % 60) return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` } } // 视频状态类 @Observed class VideoState { isPlaying: boolean = false isFinish: boolean = false isLoading: boolean = true isVoiceOff: boolean = false isControlsVisible: number = 0 currentTime: number = 0 duration: number = 0 speed: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X } // 动画配置类 class AnimationOption { duration: number = 300 curve: Curve = Curve.EaseInOut delay: number = 0 iterations: number = 1 playMode: PlayMode = PlayMode.Normal } 交互体检:用户操作流程:点击video→进度条及播放按钮隐藏→再次点击video→进度条及播放按钮显示。5.方案成果总结(1) 通过自定义封装组件一体化设计统一视频预览播放器的样式,减少开发人员的开发成本(2) 清晰传值方式,使得开发者很容易的使用这个视频预览组件(3) 加载时的图片预览可以使得加载时不是默认的黑屏,提高用户体验,统一的点击隐藏与显示效果,完美实现了客户对于视频播放器ui的需求,最终实现原生视频播放器video的优化升级。
-
一、问题说明A页面快速进入B页面后,B页面快速进入后台,即整个App都处于后台中,这个时候小窗开启;点击小窗或者点击App桌面图标唤起应用,这个时候快速把B页面关闭掉让App快速进入后台,此时A页面的不能自动拉起画中画。(这里所说的A、B页面同属于一个Ability)二、原因分析先贴上关闭画中画的代码:/*** 关闭画中画*/async stopPip() { try { this.autoStartPip = false if(this.pipController){ this.pipController?.off('stateChange'); this.pipController?.off('controlEvent'); await this.pipController?.stopPiP(); } } catch (e) { console.log('PipManager','关闭画中画: '+json.stringify(e)) }}1.本地画中画关闭日志、拉起日志分析PipManager, 关闭画中画: {"code":1300012},发现多次打印关闭画中画日志2.官方文档查询,画中画拉起和关闭怎么使用的三、解决思路1.断点查找关闭画中画日志的调用栈,减少不必要的关闭动作2.画中画的stateChange、controlEvent事件监听放在画中画关闭成功的回调里面3.页面B不需要画中画,进行关闭处理。四、解决方法关闭画中画方法调整如下:/** * 关闭画中画 */async stopPip(stopPipFinished?:()=>void) { if (this.pipController) { this.pipController.stopPiP() .then(() => { this.pipController?.off('stateChange'); this.pipController?.off('controlEvent'); console.log('PipManager', "关闭画中画成功 ") }) .catch((e: BusinessError) => { console.log('PipManager', '关闭画中画 error:' + json.stringify(e)) }) .finally(() => { this.autoStartPip = false stopPipFinished?.() console.log('PipManager', "stopPip-finally方法执行 ") }) } else { this.autoStartPip = false stopPipFinished?.() }}再开启画中画之前将之前的关闭,一定要在关闭结束后再开启画中画:this.stopPip(()=> { //开启画中画})画中画的开启参照官方文档:
-
问题说明:过去我们遇到了什么麻烦?在HarmonyOS此前的版本中,开发者在使用图标、矢量图形或自定义组件时,常常会遇到以下几个痛点:动效复用困难:如果想为多个图标赋予相同的交互动效(如点击时缩放、旋转),开发者往往需要为每个图标单独编写冗长的animate代码或状态机逻辑。这不仅导致代码冗余,维护起来更是噩梦,修改一个动效需要找到所有使用的地方逐一更改。效果叠加不便捷:为图形添加阴影或渐变效果通常需要嵌套多个组件或使用复杂的Decoration配置。尤其是动态阴影(如根据按压状态改变阴影深度),需要手动绑定数据变化和UI更新,逻辑繁琐。状态管理不连贯:“禁用”状态是一个常见的组件状态。过去,要实现禁用状态,我们通常需要手动切换图标资源(如从彩色切到灰色),并取消所有的交互动效。这个过程是割裂的,动效的禁用和样式的更改需要分开处理,不够原子化。设计一致性挑战:由于上述实现的复杂性,不同开发者甚至同一开发者在不同页面实现的相似效果都可能存在细微差异,难以保证整个应用体验的统一性,与设计稿的还原度也会大打折扣。简单来说,过去的开发模式是 “手动” 和 “分散” 的,缺乏一种声明式的、可复用的范式来统一管理图形的视觉效果和交互反馈。原因分析:为什么鸿蒙6.0要引入这些新特性?HarmonyOS 6.0 对 SymbolGlyph 的增强,其核心目的是为了解决上述问题,背后的原因可以归结为三点:提升开发效率与代码可维护性:通过将常见的视觉效果(阴影、渐变)和交互逻辑(动效、禁用)内置化、配置化,开发者无需再编写重复的样板代码。这极大地简化了开发流程,缩短了开发时间,并且让代码更加清晰、易于维护。强化设计系统与开发的一致性:SymbolGlyph 本身也是设计 tokens 的工程化体现。将这些效果作为其固有属性,使得设计师在设计规范中定义的阴影值、渐变色、微动效能够被精准、方便地落地到代码中,保证了设计与实现的高度统一。赋能更流畅细腻的交互体验:用户对应用的体验要求越来越高,流畅自然的动效和恰当的视觉反馈是提升应用品质的关键。系统级的支持可以优化这些效果的渲染性能,并提供更稳定流畅的动画表现,从而让应用显得更加精致和高端。本质上,这是鸿蒙设计系统(HarmonyOS Design)走向成熟,并更加注重开发者体验和用户体验双提升的重要标志。解决思路:如何利用新特性优雅地开发?HarmonyOS 6.0 为 SymbolGlyph 组件提供了一系列新的属性方法来应对上述挑战。我们的解决思路从“配置”代替“编码”出发。解决方案:属性1.渐变色效果(shaderStyle)通过shaderStyle设置SymbolGlyph组件的渐变色效果。shaderStyle(shaders: Array<ShaderStyle | undefined> | ShaderStyle)使用案例@Entry@Componentstruct Index {@State message: string = ‘Hello World’;linearGradientOptions1: LinearGradientOptions = {angle: 45,colors: [[Color.Red, 0.0], [Color.Blue, 0.3], [Color.Green, 0.5]]};linearGradientOptions2: LinearGradientOptions = {direction: GradientDirection.LeftTop,colors: [[Color.Red, 0.0], [Color.Blue, 0.3], [Color.Green, 0.5]],repeating: true,};radialGradientOptions: RadialGradientOptions = {center: [“50%”, “50%”],radius: “20%”,colors: [[Color.Red, 0.0], [Color.Blue, 0.3], [Color.Green, 0.5]],repeating: true,};build() {Column() {Row() {Column() {Text(‘angle为45°的线性渐变’).fontSize(18).fontColor(0xCCCCCC).textAlign(TextAlign.Center)SymbolGlyph(r('sys.symbol.ohos_folder_badge_plus')) .fontSize(96) .shaderStyle([new LinearGradientStyle(this.linearGradientOptions1)]) } .margin({ right: 20 }) Column() { Text('LeftTop的线性渐变') .fontSize(18) .fontColor(0xCCCCCC) .textAlign(TextAlign.Center) SymbolGlyph(r(‘sys.symbol.ohos_folder_badge_plus’)).fontSize(96).shaderStyle([new LinearGradientStyle(this.linearGradientOptions2)])}.margin({ right: 20 })} Row() { Column() { Text('径向渐变') .fontSize(18) .fontColor(0xCCCCCC) .textAlign(TextAlign.Center) SymbolGlyph($r('sys.symbol.ohos_folder_badge_plus')) .fontSize(96) .shaderStyle([new RadialGradientStyle(this.radialGradientOptions)]) } .margin({ right: 20 }) Column() { Text('纯色') .fontSize(18) .fontColor(0xCCCCCC) .textAlign(TextAlign.Center) SymbolGlyph($r('sys.symbol.ohos_folder_badge_plus')) .fontSize(96) .shaderStyle([new ColorShaderStyle(Color.Red)]) } .margin({ right: 20 }) Column() { Text('线性和径向渐变') .fontSize(18) .fontColor(0xCCCCCC) .textAlign(TextAlign.Center) SymbolGlyph($r('sys.symbol.ohos_folder_badge_plus')) .fontSize(96) .shaderStyle([ new LinearGradientStyle(this.linearGradientOptions2), new LinearGradientStyle(this.linearGradientOptions2), new RadialGradientStyle(this.radialGradientOptions) ]) .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_OPACITY) } .margin({ right: 20 }) } Row() { Column() { Text('数组单层渐变') .fontSize(18) .fontColor(0xCCCCCC) .textAlign(TextAlign.Center) SymbolGlyph($r('sys.symbol.ohos_folder_badge_plus')) .fontSize(96) .shaderStyle([ new LinearGradientStyle(this.linearGradientOptions2), ]) .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_OPACITY) }.margin({ right: 20 }) Column() { Text('非数组覆盖全部') .fontSize(18) .fontColor(0xCCCCCC) .textAlign(TextAlign.Center) SymbolGlyph($r('sys.symbol.ohos_folder_badge_plus')) .fontSize(96) .shaderStyle(new RadialGradientStyle(this.radialGradientOptions)) .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_OPACITY) }.margin({ right: 20 }) Column() { Text('首层为默认') .fontSize(18) .fontColor(0xCCCCCC) .textAlign(TextAlign.Center) SymbolGlyph($r('sys.symbol.ohos_folder_badge_plus')) .fontSize(96) .shaderStyle([ undefined, new LinearGradientStyle(this.linearGradientOptions2), ]) .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_OPACITY) }.margin({ right: 20 }) } } .margin({ left: 20, top: 50 })}}2.阴影效果(symbolShadow)通过symbolShadow设置SymbolGlyph组件的阴影效果。symbolShadow(shadow: Optional<ShadowOptions>)使用案例(通过设置动效和阴影)// xxx.ets@Entry@Componentstruct Index {@State isActive: boolean = true;@State triggerValueReplace: number = 0;@State triggerValueReplace1: number = 0;@State triggerValueReplace2: number = 0;@State renderMode: number = 1;replaceFlag: boolean = true;replaceFlag1: boolean = true;replaceFlag2: boolean = true;options: ShadowOptions = {radius: 10.0,color: Color.Blue,offsetX: 10,offsetY: 10,};build() {Column() {Row() {Column() {Text(“可变颜色动效”)SymbolGlyph($r(‘sys.symbol.ohos_wifi’)).fontSize(96).symbolEffect(new HierarchicalSymbolEffect(EffectFillStyle.ITERATIVE), this.isActive)Button(this.isActive ? ‘关闭’ : ‘播放’).onClick(() => {this.isActive = !this.isActive;})}.margin({ right: 20 })Column() {Text(“替换动效”)SymbolGlyph(this.replaceFlag ? $r(‘sys.symbol.checkmark_circle’) : $r(‘sys.symbol.repeat_1’)).fontSize(96).symbolEffect(new ReplaceSymbolEffect(EffectScope.WHOLE), this.triggerValueReplace)Button(‘trigger’).onClick(() => {this.replaceFlag = !this.replaceFlag;this.triggerValueReplace = this.triggerValueReplace + 1;})}.margin({ right: 20 })} Row() { Column() { Text("禁用动效") SymbolGlyph(this.replaceFlag1 ? $r('sys.symbol.eye_slash') : $r('sys.symbol.eye')) .fontSize(96) .renderingStrategy(this.renderMode) .symbolEffect(new DisableSymbolEffect(EffectScope.LAYER), this.triggerValueReplace1) Button('trigger') .onClick(() => { this.replaceFlag1 = !this.replaceFlag1; this.triggerValueReplace1 = this.triggerValueReplace1 + 1; }) } .margin({ right: 20 }) Column() { Text("快速替换动效") SymbolGlyph(this.replaceFlag2 ? $r('sys.symbol.checkmark_circle') : $r('sys.symbol.repeat_1')) .fontSize(96) .symbolEffect(new QuickReplaceSymbolEffect(EffectScope.WHOLE), this.triggerValueReplace2) Button('trigger') .onClick(() => { this.replaceFlag2 = !this.replaceFlag2; this.triggerValueReplace2 = this.triggerValueReplace2 + 1; }) } .margin({ right: 20 }) Column() { Text("阴影能力") SymbolGlyph($r('sys.symbol.ohos_wifi')) .fontSize(96) .symbolEffect(new HierarchicalSymbolEffect(EffectFillStyle.ITERATIVE), this.isActive) .symbolShadow(this.options) Button(this.isActive ? '关闭' : '播放') .onClick(() => { this.isActive = !this.isActive; }) } .margin({ right: 20 }) } } .margin({ left: 45, top: 50 })}}设置渲染和动效策略使用案例// xxx.ets@Entry@Componentstruct Index {build() {Column() {Row() {Column() {Text(“Light”)SymbolGlyph($r(‘sys.symbol.ohos_trash’)).fontWeight(FontWeight.Lighter).fontSize(96)} Column() { Text("Normal") SymbolGlyph($r('sys.symbol.ohos_trash')) .fontWeight(FontWeight.Normal) .fontSize(96) } Column() { Text("Bold") SymbolGlyph($r('sys.symbol.ohos_trash')) .fontWeight(FontWeight.Bold) .fontSize(96) } } Row() { Column() { Text("单色") SymbolGlyph($r('sys.symbol.ohos_folder_badge_plus')) .fontSize(96) .renderingStrategy(SymbolRenderingStrategy.SINGLE) .fontColor([Color.Black, Color.Green, Color.White]) } Column() { Text("多色") SymbolGlyph($r('sys.symbol.ohos_folder_badge_plus')) .fontSize(96) .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR) .fontColor([Color.Black, Color.Green, Color.White]) } Column() { Text("分层") SymbolGlyph($r('sys.symbol.ohos_folder_badge_plus')) .fontSize(96) .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_OPACITY) .fontColor([Color.Black, Color.Green, Color.White]) } } Row() { Column() { Text("无动效") SymbolGlyph($r('sys.symbol.ohos_wifi')) .fontSize(96) .effectStrategy(SymbolEffectStrategy.NONE) } Column() { Text("整体缩放动效") SymbolGlyph($r('sys.symbol.ohos_wifi')) .fontSize(96) .effectStrategy(SymbolEffectStrategy.SCALE) } Column() { Text("层级动效") SymbolGlyph($r('sys.symbol.ohos_wifi')) .fontSize(96) .effectStrategy(SymbolEffectStrategy.HIERARCHICAL) } } }}}
-
router.pushUrl({ url:url }) this.getUIContext().getRouter().pushNamedRoute({ name:"qrCode" })//都不管用
-
一、问题说明在折叠屏和 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. 效果图:
上滑加载中
推荐直播
-
华为云码道Agent集成与鸿蒙实战2026/08/11 周二 19:00-21:00
王一男-华为云码道产品规划专家;李炎-华为云码道产品专家;彭江敏-华为云鸿蒙端云一体化开发专家
本次直播带你解读华为云码道7月份产品新特性、新功能。更有专家演示码道Agent Space × 钉钉机器集成实战,从0到1打通消息通道;码道鸿蒙端云一体化实战,快速搭建员工签到系统。
回顾中 -
华为云开发者AI素养直播课·第五期2026/09/04 周五 16:00-18:00
林华鼎-华为云AI开发者运营负责人;蒋春阳-华为云AI开发者案例开发专家
本期直播内容: AI工具体验营 · 第5-8课连讲。Agent-Team 多智能体协作完成毕业设计实践
回顾中 -
华为云开发者AI素养ClassRoom·第六期2026/09/08 周二 19:00-20:00
樊渊-2026华为软件挑战赛冠军
高手来了:看软挑高手解析二维排样问题—从工业难题到算法突破
回顾中
热门标签