HarmonyOS 鸿蒙Next技术问题解析 第13期

HarmonyOS 鸿蒙Next技术问题解析 第13期 向所有参与社区互助的开发者致以最诚挚的感谢!

社区的蓬勃发展,离不开每一位积极参与者的贡献。本期“答开发者问”栏目,精选自广大热心开发者针对提问帖所贡献的众多优质答复之中。它们不仅是智慧与经验的璀璨结晶,更是“众人拾柴火焰高”这一真理的生动体现。

在此,我们由衷地感谢每一位热心参与、乐于分享的开发者,是你们的热情与智慧,让这个社区充满了生机与活力,每一次的解答都是对技术探索精神的最好诠释。同时,我们也诚挚邀请更多的开发者加入到这场智慧碰撞的盛宴中来。无论是抛出难题寻求解答,还是慷慨解囊分享经验,您的每一份参与都将为鸿蒙开发者社区注入新的活力,推动我们共同前行,在技术的海洋中扬帆远航。

本期问题如下:

  1. ListItemGroup 的Header如何动态设置?
  2. 快递进度条效果如何实现?
  3. 怎么实现一个扇形的油表盘进度条?
  4. 如何分别监听设备的横竖屏旋转和设备屏幕大小变化?
  5. 如何实现图片逐个翻转效果?

答开发者问系列汇总:

“答开发者问”系列汇总(持续更新中…)

往期问题回顾:

“答开发者问”之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

10 回复

问题二:快递进度条效果如何实现?

具体效果参考下图,左侧线条需要随着内容高度而变化。

5D918875-4C4D-4384-8670-4E60590444B4.jpg

解决方案:

使用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)

打卡学习

如何实现相机快门动画?

问题五:如何实现逐个翻转效果?

问题描述:

如下图所示,下图中的图片来自于同一个数组,当前是同时翻转的,请问如何实现逐个翻转。

D4614A8B-6BC7-4AFA-9F63-CCAFBB737DF1.gif

解决方案:

在页面打开时就自动翻转,可以利用组件挂载卸载事件,此处是显示时触发回调,使用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个圆的圆形进度条,效果类似下图,需要在中间写文字。

E8BBC1EC-C740-44BA-98F0-F5588AE03C6D.png

解决方案:

可以使用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是华为新一代操作系统,采用微内核架构和分布式设计。关键技术包括:

  1. 分布式软总线:实现设备间低时延通信
  2. 原子化服务:独立功能模块可自由组合
  3. 确定性时延引擎:保障关键任务响应
  4. 方舟编译器:提升应用性能
  5. 安全微内核:TEE级别安全防护

开发者需注意:Next版本仅支持ArkTS语言开发,不再兼容Android APK。系统API有较大调整,需使用DevEco Studio 4.0及以上版本进行适配开发。

关于HarmonyOS Next开发中的几个技术问题,以下是我的专业解答:

  1. ListItemGroup Header动态设置: 可以通过@State装饰器绑定数据源,在build函数中使用条件渲染或map方法动态生成Header内容。例如:
[@State](/user/State) headers: string[] = ['Header1', 'Header2'];
...
ListItemGroup({ header: this.headers[index] }) 
  1. 快递进度条实现: 推荐使用<Progress>组件结合自定义样式实现。关键点:
  • 设置progressWidth控制进度宽度
  • 使用linear渐变效果
  • 添加节点标记可通过绝对定位实现
  1. 扇形油表盘实现方案: 使用Canvas绘制扇形:
// 绘制扇形路径
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.lineTo(x, y);
ctx.closePath();
// 填充颜色
ctx.fillStyle = color;
ctx.fill();
  1. 屏幕方向与尺寸监听:
  • 屏幕旋转:通过window.on('orientationChange')监听
  • 尺寸变化:使用window.on('windowSizeChange')事件 建议在aboutToAppear中注册监听,在aboutToDisappear中取消
  1. 图片逐个翻转效果: 使用<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上运行稳定。具体实现时可根据实际需求调整参数和动画细节。

回到顶部