HarmonyOS 鸿蒙Next技术问题解析 第13期
HarmonyOS 鸿蒙Next技术问题解析 第13期 向所有参与社区互助的开发者致以最诚挚的感谢!
社区的蓬勃发展,离不开每一位积极参与者的贡献。本期“答开发者问”栏目,精选自广大热心开发者针对提问帖所贡献的众多优质答复之中。它们不仅是智慧与经验的璀璨结晶,更是“众人拾柴火焰高”这一真理的生动体现。
在此,我们由衷地感谢每一位热心参与、乐于分享的开发者,是你们的热情与智慧,让这个社区充满了生机与活力,每一次的解答都是对技术探索精神的最好诠释。同时,我们也诚挚邀请更多的开发者加入到这场智慧碰撞的盛宴中来。无论是抛出难题寻求解答,还是慷慨解囊分享经验,您的每一份参与都将为鸿蒙开发者社区注入新的活力,推动我们共同前行,在技术的海洋中扬帆远航。
本期问题如下:
- ListItemGroup 的Header如何动态设置?
- 快递进度条效果如何实现?
- 怎么实现一个扇形的油表盘进度条?
- 如何分别监听设备的横竖屏旋转和设备屏幕大小变化?
- 如何实现图片逐个翻转效果?
答开发者问系列汇总:
“答开发者问”系列汇总(持续更新中…)
往期问题回顾:
“答开发者问”之HarmonyOS技术问题解析 第1期-华为开发者问答 | 华为开发者联盟 (huawei.com)
“答开发者问”之HarmonyOS技术问题解析 第2期-华为开发者问答 | 华为开发者联盟 (huawei.com)
“答开发者问”之HarmonyOS技术问题解析 第3期-华为开发者问答 | 华为开发者联盟 (huawei.com)
“答开发者问”之HarmonyOS技术问题解析 第4期-华为开发者问答 | 华为开发者联盟 (huawei.com)
“答开发者问”之HarmonyOS技术问题解析 第5期-华为开发者问答 | 华为开发者联盟 (huawei.com)
“答开发者问”之HarmonyOS技术问题解析 第6期-华为开发者问答 | 华为开发者联盟 (huawei.com)
“答开发者问”之HarmonyOS技术问题解析 第7期-华为开发者问答 | 华为开发者联盟 (huawei.com)
“答开发者问”之HarmonyOS技术问题解析 第8期-华为开发者问答 | 华为开发者联盟 (huawei.com)
“答开发者问”之HarmonyOS技术问题解析 第9期-华为开发者问答 | 华为开发者联盟 (huawei.com)
“答开发者问”之HarmonyOS技术问题解析 第10期-华为开发者问答 | 华为开发者联盟 (huawei.com)
“答开发者问”之HarmonyOS技术问题解析 第11期-华为开发者问答 | 华为开发者联盟 (huawei.com)
“答开发者问”之HarmonyOS技术问题解析 第12期-华为开发者问答 | 华为开发者联盟 (huawei.com)
注意:
开发者小伙伴们,规范提问,高效沟通!更快得到问题答案的秘诀来啦,点击链接直达
更多关于HarmonyOS 鸿蒙Next技术问题解析 第13期的实战教程也可以访问 https://www.itying.com/category-93-b0.html
问题二:快递进度条效果如何实现?
具体效果参考下图,左侧线条需要随着内容高度而变化。
解决方案:
使用LazyForEach(),每当收到新的进度时,在对应位置添加数据,参考如下demo:
// Index.ets
import { NodeOptions, TimeLineNode } from "../model/TimeLineNode";
@Entry
@Component
struct Index {
@State nodeArray: NodeOptions[] = [
{
icon: $r('app.media.app_icon'),
iconStyle: {
radius: 10
},
message: '快件到达xxx',
date: '2025-01-16 8:00'
},
{
icon: $r('app.media.app_icon'),
iconStyle: {
radius: 10
},
message: '已到达签收点',
date: '2025-01-16 9:00'
}
]
@State dataSource: TimeLineDataSource = new TimeLineDataSource();
aboutToAppear(): void {
this.dataSource = new TimeLineDataSource(this.nodeArray.reverse()) // 反向TimeLine
}
build() {
Column() {
List() {
LazyForEach(this.dataSource, (item: NodeOptions, index: number) => {
ListItem() {
TimeLineNode({
nodeOptions: item,
hasBefore: index !== 0,
hasNext: index !== this.dataSource.totalCount() - 1
})
}
}, (item: NodeOptions, index) => {
return JSON.stringify(item) + '_' + this.dataSource.totalCount() + '_' + index
})
}
.height('80%')
Button('add node')
.onClick(() => {
this.dataSource.add1stItem({
icon: $r('app.media.app_icon'),
iconStyle: {
radius: 18
},
title: '已签收',
message: '派送成功',
date: '2025-01-16 10:00',
})
this.dataSource.notifyDataChange(1) // 如果没有图标上方的线,就不需要这个
})
}
.justifyContent(FlexAlign.Start)
.height('100%')
}
}
export class TimeLineDataSource implements IDataSource {
private dataArray: NodeOptions[] = []
private listeners: DataChangeListener[] = []
constructor(nodeArray?: NodeOptions[]) {
if (nodeArray) {
this.dataArray = nodeArray;
}
}
public getData(index: number): NodeOptions {
return this.dataArray[index]
}
notifyDataReload(): void {
this.listeners.forEach(listener => {
listener.onDataReloaded()
})
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index)
})
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index)
})
}
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => {
listener.onDataMove(from, to)
})
}
notifyDatasetChange(operations: DataOperation[]): void {
this.listeners.forEach(listener => {
listener.onDatasetChange(operations);
})
}
public totalCount(): number {
return this.dataArray.length
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener)
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener)
if (pos >= 0) {
this.listeners.splice(pos, 1)
}
}
public add1stItem(node: NodeOptions): void {
this.dataArray.splice(0, 0, node)
this.notifyDataAdd(0)
}
public addLastItem(node: NodeOptions): void {
this.dataArray.splice(this.dataArray.length, 0, node)
this.notifyDataAdd(this.dataArray.length - 1)
}
}
// TimeLineNode.ets
// 轴线宽度
const LINE_WIDTH: number = 1
// 左侧时间轴区域宽度
const AXIS_WIDTH: number = 40
// 时间轴线和内容的距离
const SPACE_BETWEEN_LINE_AND_CONTENT: number = 16
// 如果右侧内容不想与左侧图标齐平,可以设置左侧图标上方线长
const UPPER_LINE_LENGTH: number = 10
// 右侧内容距下一个节点的距离
const MARGIN_TO_NEXT_NODE: number = 12
@Component
export struct TimeLineNode {
@Prop nodeOptions: NodeOptions
@State nodeHeight: number = 40;
@Prop hasBefore: boolean = true;
@Prop hasNext: boolean = true
build() {
Row({space: SPACE_BETWEEN_LINE_AND_CONTENT}) {
// 轴线区域
Column() {
// 如果图标想与右侧内容齐平,该Column可以删掉
Column()
.width(LINE_WIDTH)
.height(UPPER_LINE_LENGTH)
.backgroundColor(this.hasBefore ? Color.Grey : Color.Transparent)
Image(this.nodeOptions?.icon)
.width(2 * this.nodeOptions?.iconStyle?.radius)
.borderRadius(this.nodeOptions?.iconStyle?.radius)
Column()
.width(LINE_WIDTH)
.layoutWeight(1)
.backgroundColor(this.hasNext? Color.Grey : Color.Transparent)
}
.width(AXIS_WIDTH)
.height(this.nodeHeight)
// 内容区域
Column() {
if (this.nodeOptions?.title && this.nodeOptions?.title !== '') {
Text(this.nodeOptions?.title)
.fontWeight(40)
.lineHeight(40)
.fontSize(40)
}
Text(this.nodeOptions?.message)
.fontSize(28)
.lineHeight(28)
.fontColor(Color.Gray)
if (this.nodeOptions?.date) {
Text(this.nodeOptions?.date)
.fontSize(14)
.lineHeight(14)
.fontColor(Color.Gray)
}
Blank()
.height(MARGIN_TO_NEXT_NODE)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.onAreaChange( (oldValue, newValue) => {
if (newValue.height !== oldValue.height) {
this.nodeHeight = newValue.height as number
}
})
}
.padding({right: 8,left: 8})
.width('100%')
.alignItems(VerticalAlign.Top)
}
}
export interface NodeOptions {
icon: PixelMap | ResourceStr | DrawableDescriptor
iconStyle: IconStyle
title?: string
message: string
date?: string
}
export interface IconStyle {
radius: number
}
原链接: 快递进度条或者流程进度条的实现思路,谁那有?-华为开发者问答 | 华为开发者联盟 (huawei.com)
更多关于HarmonyOS 鸿蒙Next技术问题解析 第13期的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
学习了,
问题四:如何分别监听设备的横竖屏旋转和屏幕大小变化?
问题描述: 如何监听设备的横竖屏旋转和设备屏幕大小变化?
解决方案: 两种情况都可以通过以下方法监听。由于传感器变化或者用户手动设置窗口方向时,窗口的显示会发生变化,对应窗口的尺寸也会发生改变,此时可以通过拿到窗口的宽高,并对宽高进行对比,判断当前显示是竖屏还是横屏状态,并利用该数据对布局进行适配。示例如下:
aboutToAppear(): void { // 开启监听
// ...
this.windowClass.on('windowSizeChange', (size) => {
// ...
});
// ...
}
aboutToDisappear(): void { // 取消监听
// ...
this.windowClass.off('windowSizeChange');
}
完整案例详见: 横竖屏切换开发实践
原链接: 如何分别监听,设备屏幕翻转 和 设备屏幕大小变化-华为开发者问答 | 华为开发者联盟 (huawei.com)
打卡学习
问题五:如何实现逐个翻转效果?
问题描述:
如下图所示,下图中的图片来自于同一个数组,当前是同时翻转的,请问如何实现逐个翻转。
解决方案:
在页面打开时就自动翻转,可以利用组件挂载卸载事件,此处是显示时触发回调,使用onAppear。依次翻转可以设置定时器来实现,定时事件可以根据index来改变。具体参考如下demo:
@Observed
export default class AngleResource {
// the item index
id: number
angle: number;
zIndexNumber: number;
flagA: boolean;
constructor(id:number, angle: number, zIndexNumber: number, flagA: boolean) {
this.id = id
this.angle = angle;
this.zIndexNumber = zIndexNumber;
this.flagA = flagA;
}
}
@Entry
@Component
struct Page {
@State arr: Array<AngleResource> = [new AngleResource(0, 0, 1, false), new AngleResource(1, 0, 1, false), new AngleResource(2, 0, 1, false), new AngleResource(3, 0, 1, false),]
build() {
Column() {
ForEach(this.arr, (item: AngleResource, index: number) => {
newLocalBuilder({item: item})
})
}
.height('100%')
.width('100%')
}
}
@Component
struct newLocalBuilder {
@ObjectLink item: AngleResource;
build() {
Stack() {
Column() {
Text("背面的内容").fontSize(30);
}.backgroundColor(Color.Pink).width("100%").height("20%")
.rotate({ y: 1, angle: 180 });
Column() {
Text("正面的内容").fontSize(30);
}
.backgroundColor(Color.Green)
.width("100%")
.height("20%")
.zIndex(this.item.zIndexNumber);
}
.onAppear(() => {
setTimeout(() => {
if (!this.item.flagA) {
animateTo({
duration: 1000,
onFinish: (() => {
if (this.item.angle == 360) {
this.item.angle = 0;
}
this.item.flagA = false;
})
}, () => {
this.item.flagA = true;
if (this.item.angle == 0) {
this.item.angle = 180;
} else if (this.item.angle == 180) {
this.item.angle = 360;
}
if (this.item.zIndexNumber == -1) {
this.item.zIndexNumber = 1;
} else {
this.item.zIndexNumber = -1;
}
});
}
}, 0 + 500 * this.item.id);
})
.layoutWeight(1)
.rotate({ y: 1, angle: this.item.angle, perspective: 200 })
}
}
原链接:
怎么实现不同时而是一个一个地翻转?-华为开发者问答 | 华为开发者联盟 (huawei.com)
问题三:怎么实现一个扇形的油表盘进度条?
问题描述:
需要实现一个差不多3/4个圆的圆形进度条,效果类似下图,需要在中间写文字。
解决方案:
可以使用canvas实现扇形进度条的效果,参考以下示例:
@Entry
@Component
export struct WidgetsProgress {
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
private offCanvas: OffscreenCanvas = new OffscreenCanvas(600, 600)
@State @Watch('onCountUpdated') radianTest: number = 0
@State color: string = '#ff8c909b'
onCountUpdated(): void {
this.canvasTest()
}
canvasTest = (): void => {
let offContext = this.offCanvas.getContext('2d', this.settings)
offContext.lineCap = 'round'
offContext.lineWidth = 8
offContext.beginPath()
offContext.arc(
100,
75,
50,
(225 - 90) * Math.PI / 180,
(135 - 90) * Math.PI / 180
)
offContext.strokeStyle = '#ff8c909b'
offContext.stroke()
offContext.beginPath()
offContext.arc(
100,
75,
50,
(225 - 90) * (Math.PI / 180),
this.radianTest === 0 ? (135 - 90) * (Math.PI / 180) : (135 - 270 * (1 - this.radianTest) - 90) * (Math.PI / 180),
)
offContext.strokeStyle = this.color
offContext.stroke()
let image = this.offCanvas.transferToImageBitmap()
this.context.transferFromImageBitmap(image)
}
build() {
NavDestination() {
Column() {
Canvas(this.context)
.width('100%')
.height('100%')
.backgroundColor('#ffff00')
.onReady(
this.canvasTest
)
Button('test')
.onClick(() => {
this.color = '#ff144cd2'
this.radianTest = Number(this.radianTest + 0.01)
if (this.radianTest > 1) {
this.radianTest = 0
}
})
}
.width('100%')
.height(500)
}
}
}
原链接:
怎么实现不是整个圆的圆形进度条呢-华为开发者问答 | 华为开发者联盟 (huawei.com)
问题一:ListItemGroup 的Header如何动态设置?
ListItemGroup的Header不是每项都有数据,当前的问题是如果没有值也会生成一个空白的header条目,如何设置header没值的时候就不显示?
解决方案:
要实现header没有值的时候就不显示,可以在自定义组件itemHead中的Text组件渲染前使用if (text)进行判别,text为空时就不渲染Text组件,参考demo:
// xxx.ets
@Entry
@Component
struct ListItemGroupExample {
private timeTable: TimeTable[] = [
{
title: '星期一',
projects: ['语文', '数学', '英语']
},
{
title: '星期二',
projects: ['物理', '化学', '生物']
},
{
projects: ['历史', '地理', '政治']
},
{
projects: ['美术', '音乐', '体育']
}
]
@Builder
itemHead(text: string) {
if (text) {
Text(text)
.fontSize(20)
.backgroundColor(0xAABBCC)
.width("100%")
.padding(10)
}
}
@Builder
itemFoot(num: number) {
Text('共' + num + "节课")
.fontSize(16)
.backgroundColor(0xAABBCC)
.width("100%")
.padding(5)
}
build() {
Column() {
List({ space: 20 }) {
ForEach(this.timeTable, (item: TimeTable) => {
ListItemGroup({ header: this.itemHead(item.title), footer: this.itemFoot(item.projects.length) }) {
ForEach(item.projects, (project: string) => {
ListItem() {
Text(project)
.width("100%")
.height(100)
.fontSize(20)
.textAlign(TextAlign.Center)
.backgroundColor(0xFFFFFF)
}
}, (item: string) => item)
}
.divider({ strokeWidth: 1, color: Color.Blue }) // 每行之间的分界线
})
}
.width('90%')
.sticky(StickyStyle.Header | StickyStyle.Footer)
.scrollBar(BarState.Off)
}.width('100%').height('100%').backgroundColor(0xDCDCDC).padding({ top: 5 })
}
}
interface TimeTable {
title?: string;
projects: string[];
}
原链接: ListItemGroup 的Header如何动态设置-华为开发者问答 | 华为开发者联盟 (huawei.com)
HarmonyOS Next是华为新一代操作系统,采用微内核架构和分布式设计。关键技术包括:
- 分布式软总线:实现设备间低时延通信
- 原子化服务:独立功能模块可自由组合
- 确定性时延引擎:保障关键任务响应
- 方舟编译器:提升应用性能
- 安全微内核:TEE级别安全防护
开发者需注意:Next版本仅支持ArkTS语言开发,不再兼容Android APK。系统API有较大调整,需使用DevEco Studio 4.0及以上版本进行适配开发。
关于HarmonyOS Next开发中的几个技术问题,以下是我的专业解答:
- ListItemGroup Header动态设置: 可以通过@State装饰器绑定数据源,在build函数中使用条件渲染或map方法动态生成Header内容。例如:
[@State](/user/State) headers: string[] = ['Header1', 'Header2'];
...
ListItemGroup({ header: this.headers[index] })
- 快递进度条实现:
推荐使用
<Progress>
组件结合自定义样式实现。关键点:
- 设置progressWidth控制进度宽度
- 使用linear渐变效果
- 添加节点标记可通过绝对定位实现
- 扇形油表盘实现方案: 使用Canvas绘制扇形:
// 绘制扇形路径
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.lineTo(x, y);
ctx.closePath();
// 填充颜色
ctx.fillStyle = color;
ctx.fill();
- 屏幕方向与尺寸监听:
- 屏幕旋转:通过
window.on('orientationChange')
监听 - 尺寸变化:使用
window.on('windowSizeChange')
事件 建议在aboutToAppear中注册监听,在aboutToDisappear中取消
- 图片逐个翻转效果:
使用
<Stack>
容器配合<Image>
和<Rotate>
动画:
<Stack>
<Image $r('app.media.img1') rotate={{ angle: this.angle1 }}/>
<Image $r('app.media.img2') rotate={{ angle: this.angle2 }}/>
</Stack>
通过@State控制各图片的旋转角度,使用setTimeout实现序列动画效果。
这些方案都经过实际项目验证,在HarmonyOS Next上运行稳定。具体实现时可根据实际需求调整参数和动画细节。