简介

在OpenHarmony应用开发中,设置循环流体边框动效是较为常见的场景。下文会通过详细示例来描述如何实现。

开发环境

DevEco Studio: DevEco Studio 6.0.1 Release(Build Version: 6.0.1.260)

系统: OpenHarmony 6.0.0.47

设备: DAYU200(rk3568)

最佳实践示例

@Entry
@Component
struct BorderTest1 {
  build() {
    Column() {
      FlowBorder({
        widthFlow: 260,
        heightFlow: 140,
        lineWidth: 4,
        cornerRadius: 18,
        color: '#00E5FF',
        segLen: 36, // 亮段长度(调小就是“一小段在跑”)
        speed: 2 // 每帧前进素
      }) {
        Text('内容区')
          .fontSize(16)
          .fontColor(Color.White)
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#111')
  }
}

@Component
struct FlowBorder {
  @Prop widthFlow: number = 240
  @Prop heightFlow: number = 120
  @Prop lineWidth: number = 3
  @Prop cornerRadius: number = 16
  @Prop color: string = '#00E5FF'

  @Prop segLen: number = 36
  @Prop speed: number = 2

  @BuilderParam content?: () => void

  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)

  private timerId: number = -1
  private dashOffset: number = 0

  build() {
    Stack() {
      Column() {
        if (this.content) this.content()
      }
      .width(this.widthFlow)
      .height(this.heightFlow)
      .padding(16)
      .borderRadius(this.cornerRadius)
      .clip(true)

      Canvas(this.ctx)
        .width(this.widthFlow)
        .height(this.heightFlow)
        .hitTestBehavior(HitTestMode.Transparent)
        .onReady(() => this.start())

    }
  }

  private start(): void {
    this.stop()
    this.dashOffset = 0

    this.timerId = setInterval(() => {
      const w = this.widthFlow
      const h = this.heightFlow
      const lw = this.lineWidth

      // r 不能超过几何限制
      const r = Math.max(0, Math.min(this.cornerRadius, (Math.min(w, h) - lw) / 2))
      const rw = w - lw
      const rh = h - lw

      // 圆角矩形周长:直线部分 + 四个1/4圆 = 2πr
      const perimeter = 2 * (rw + rh - 4 * r) + 2 * Math.PI * r

      this.dashOffset = (this.dashOffset + this.speed) % perimeter
      this.draw(perimeter, r)
    }, 16)
  }

  private stop(): void {
    if (this.timerId !== -1) {
      clearInterval(this.timerId)
      this.timerId = -1
    }
  }

  private draw(perimeter: number, r: number): void {
    const ctx = this.ctx
    const w = this.widthFlow
    const h = this.heightFlow
    const lw = this.lineWidth

    const x = lw / 2
    const y = lw / 2
    const rw = w - lw
    const rh = h - lw

    const segLen = Math.max(1, Math.min(this.segLen, perimeter))
    const gapLen = Math.max(0, perimeter - segLen) // 关键:周期=perimeter,保证无缝

    ctx.clearRect(0, 0, w, h)

    // 底色边框(可选)
    ctx.save()
    ctx.beginPath()
    this.roundRectPath(ctx, x, y, rw, rh, r)
    ctx.lineWidth = lw
    ctx.strokeStyle = 'rgba(0,229,255,0.15)'
    ctx.setLineDash([])
    ctx.stroke()
    ctx.restore()

    // 流动亮段(只有一段)
    ctx.save()
    ctx.beginPath()
    this.roundRectPath(ctx, x, y, rw, rh, r)
    ctx.lineWidth = lw
    ctx.strokeStyle = this.color
    ctx.lineCap = 'round'
    ctx.lineJoin = 'round'
    ctx.setLineDash([segLen, gapLen])
    ctx.lineDashOffset = -this.dashOffset // 反方向就去掉负号
    ctx.stroke()
    ctx.restore()
  }

  // 用 arcTo 兼容地画圆角矩形路径(不依赖 roundRect)
  private roundRectPath(ctx: CanvasRenderingContext2D,
    x: number, y: number, w: number, h: number, r: number): void {

    const rr = Math.max(0, Math.min(r, w / 2, h / 2))

    ctx.moveTo(x + rr, y)
    ctx.lineTo(x + w - rr, y)
    ctx.arcTo(x + w, y, x + w, y + rr, rr)

    ctx.lineTo(x + w, y + h - rr)
    ctx.arcTo(x + w, y + h, x + w - rr, y + h, rr)

    ctx.lineTo(x + rr, y + h)
    ctx.arcTo(x, y + h, x, y + h - rr, rr)

    ctx.lineTo(x, y + rr)
    ctx.arcTo(x, y, x + rr, y, rr)

    ctx.closePath()
  }
}
Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐