PlayingIndicator.ets 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { CommonConstants } from '../common/constants/CommonConstants'
  2. import { normalizeHexColor, rgbaFromHex } from './spectrum/SpectrumRenderer'
  3. //正在播放的动画指示器
  4. @Component
  5. export struct PlayingIndicator {
  6. @Prop @Watch('onActiveChange') isActive: boolean = false
  7. @Prop indicatorSize: number = 18
  8. @Prop indicatorColor: string = ''
  9. @Prop marginRight: number = 0
  10. @Prop marginTop: number = 0
  11. @Prop marginBottom: number = 0
  12. @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR
  13. private readonly renderSettings: RenderingContextSettings = new RenderingContextSettings(true)
  14. private readonly canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.renderSettings)
  15. private timerId: number = -1
  16. private phase: number = 0
  17. private canvasWidth: number = 0
  18. private canvasHeight: number = 0
  19. aboutToAppear(): void {
  20. this.updateAnimationState()
  21. this.drawFrame()
  22. }
  23. aboutToDisappear(): void {
  24. this.stopTimer()
  25. }
  26. onActiveChange(): void {
  27. this.updateAnimationState()
  28. this.drawFrame()
  29. }
  30. private updateAnimationState(): void {
  31. if (this.isActive) {
  32. this.startTimer()
  33. return
  34. }
  35. this.stopTimer()
  36. this.phase = 0
  37. }
  38. private startTimer(): void {
  39. if (this.timerId !== -1) {
  40. return
  41. }
  42. this.timerId = setInterval(() => {
  43. this.phase += 0.58
  44. if (this.phase > Math.PI * 2) {
  45. this.phase -= Math.PI * 2
  46. }
  47. this.drawFrame()
  48. }, 88)
  49. }
  50. private stopTimer(): void {
  51. if (this.timerId === -1) {
  52. return
  53. }
  54. clearInterval(this.timerId)
  55. this.timerId = -1
  56. }
  57. private drawFrame(): void {
  58. if (this.canvasWidth <= 0 || this.canvasHeight <= 0) {
  59. return
  60. }
  61. const ctx: CanvasRenderingContext2D = this.canvasContext
  62. ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
  63. if (!this.isActive) {
  64. return
  65. }
  66. const colorSource: string = this.indicatorColor.length > 0 ? this.indicatorColor : this.themeColor
  67. const brandColor: string = normalizeHexColor(colorSource, CommonConstants.DEFAULT_THEME_COLOR)
  68. const barCount: number = 5
  69. const barWidth: number = Math.max(1.0, Math.min(1.8, this.canvasWidth * 0.075))
  70. const barGap: number = Math.max(1.0, Math.min(1.9, this.canvasWidth * 0.092))
  71. const totalWidth: number = barWidth * barCount + barGap * (barCount - 1)
  72. const startX: number = (this.canvasWidth - totalWidth) / 2
  73. const baseY: number = this.canvasHeight
  74. const minBarHeight: number = this.canvasHeight * 0.28
  75. const maxBarHeight: number = this.canvasHeight * 0.84
  76. for (let i = 0; i < barCount; i++) {
  77. const primaryWave: number = 0.5 + 0.5 * Math.sin(this.phase + i * 0.82)
  78. const secondaryWave: number = 0.5 + 0.5 * Math.sin(this.phase * 0.65 + i * 1.24 + 1.1)
  79. const level: number = 0.18 + 0.82 * (primaryWave * 0.72 + secondaryWave * 0.28)
  80. const barHeight: number = minBarHeight + (maxBarHeight - minBarHeight) * level
  81. const x: number = startX + i * (barWidth + barGap)
  82. const y: number = baseY - barHeight
  83. const topColor: string = rgbaFromHex(brandColor, 0.96)
  84. const bottomColor: string = rgbaFromHex(brandColor, 0.62)
  85. const glowColor: string = rgbaFromHex(brandColor, 0.12)
  86. const barGradient: CanvasGradient = ctx.createLinearGradient(x, y, x, baseY)
  87. barGradient.addColorStop(0, topColor)
  88. barGradient.addColorStop(1, bottomColor)
  89. ctx.fillStyle = glowColor
  90. this.fillRoundedRect(ctx, x - 0.35, y - 0.35, barWidth + 0.7, barHeight + 0.7, barWidth * 0.50)
  91. ctx.fillStyle = barGradient
  92. this.fillRoundedRect(ctx, x, y, barWidth, barHeight, barWidth * 0.50)
  93. }
  94. }
  95. private fillRoundedRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number,
  96. radius: number): void {
  97. const safeRadius: number = Math.min(radius, width * 0.5, height * 0.5)
  98. ctx.beginPath()
  99. ctx.moveTo(x + safeRadius, y)
  100. ctx.lineTo(x + width - safeRadius, y)
  101. ctx.quadraticCurveTo(x + width, y, x + width, y + safeRadius)
  102. ctx.lineTo(x + width, y + height - safeRadius)
  103. ctx.quadraticCurveTo(x + width, y + height, x + width - safeRadius, y + height)
  104. ctx.lineTo(x + safeRadius, y + height)
  105. ctx.quadraticCurveTo(x, y + height, x, y + height - safeRadius)
  106. ctx.lineTo(x, y + safeRadius)
  107. ctx.quadraticCurveTo(x, y, x + safeRadius, y)
  108. ctx.closePath()
  109. ctx.fill()
  110. }
  111. build() {
  112. Row() {
  113. Canvas(this.canvasContext)
  114. .width(this.indicatorSize)
  115. .height(this.indicatorSize)
  116. .onReady(() => {
  117. this.canvasContext.imageSmoothingEnabled = true
  118. this.canvasContext.imageSmoothingQuality = 'medium'
  119. this.drawFrame()
  120. })
  121. .onAreaChange((_oldVal: Area, newVal: Area) => {
  122. this.canvasWidth = newVal.width as number
  123. this.canvasHeight = newVal.height as number
  124. this.drawFrame()
  125. })
  126. }
  127. .width(this.indicatorSize)
  128. .height(this.indicatorSize)
  129. .margin({ right: this.marginRight, top: this.marginTop, bottom: this.marginBottom })
  130. .visibility(this.isActive ? Visibility.Visible : Visibility.None)
  131. .onAppear(() => {
  132. this.updateAnimationState()
  133. this.drawFrame()
  134. })
  135. .onDisAppear(() => {
  136. this.stopTimer()
  137. })
  138. }
  139. }