从零开始纯血鸿蒙天气预报-主界面(3)

157 阅读1分钟

易得天气

完成了15日天气预报面板UI

效果图

1、曲线/列表切换

2025-03-14 17.05.53.gif

2、列表展开/收起

2025-03-14 17.06.47.gif

页面布局

Column() {
  Divider()
    .strokeWidth(0.5)
    .color(ColorUtils.alpha($r('app.color.special_white'), 0.2))

  if (this.currentDailyWeatherType == Constants.LINE_CHART_DAILY_WEATHER) {
    // 曲线
    this.lineChartDailyWeather()
  } else if (this.currentDailyWeatherType == Constants.LIST_DAILY_WEATHER) {
    // 列表
    this.listDailyWeather()
  }
}
.padding({ top: Constants.ITEM_STICKY_HEIGHT })
.clipShape(new PathShape({ commands: this.panelPathCommands }))

曲线/列表切换动画

通过transition实现曲线/列表切换动画


.transition(TransitionEffect.opacity(0).animation({ curve: Curve.Ease, duration: 300 }))

展开/收起动画

通过animation实现展开/收起动画


.height(this.panelHeight)
.animation({ curve: Curve.Ease, duration: 300 })
get panelHeight(): number {
  if (this.currentDailyWeatherType == Constants.LINE_CHART_DAILY_WEATHER) {
    const find = this.weatherItemData?.weatherData?.forecast15?.find(it => StrUtil.isNotEmpty(it.aqi_level_name))
    return find != null ? 420 : 382
  }
  const value = this.isExpand ? Math.max(Constants.DAILY_WEATHER_ITEM_COUNT,
    this.weatherItemData?.weatherData?.forecast15?.length ?? Constants.DAILY_WEATHER_ITEM_COUNT) :
  Math.min(Constants.DAILY_WEATHER_ITEM_COUNT,
    this.weatherItemData?.weatherData?.forecast15?.length ?? Constants.DAILY_WEATHER_ITEM_COUNT)
  return Constants.ITEM_STICKY_HEIGHT + Constants.DAILY_WEATHER_ITEM_HEIGHT * value +
  Constants.DAILY_WEATHER_BOTTOM_HEIGHT
}

列表状态下的气温条

WeatherTempLineBar({
  barWidth: '100%',
  barHeight: 4,
  temp: this.isToday(item) ? this.weatherItemData?.weatherData?.observe?.temp : undefined,
  high: item?.high,
  low: item?.low,
  maxTemp: this.maxTempData()?.high,
  minTemp: this.minTempData()?.low
})
  .layoutWeight(1)
@ComponentV2
export struct WeatherTempLineBar {
  @Param @Require barWidth: Length = 0
  @Param @Require barHeight: number = 0
  @Param temp?: number = undefined
  @Param high: number = 0
  @Param low: number = 0
  @Param maxTemp: number = 0
  @Param minTemp: number = 0
  @Local colors: Array<[ResourceColor, number]> = []
  @Local pathCommands: string = ''
  @Local todayTempMarginLeft: number = 0

  build() {
    Stack() {
      Stack({ alignContent: Alignment.Start }) {
        if (this.temp) {
          Circle({ width: this.barHeight, height: this.barHeight })
            .fill($r('app.color.special_white'))
            .margin({ left: this.todayTempMarginLeft })
        }
      }
      .width('100%')
      .height(this.barHeight)
      .linearGradient({
        direction: GradientDirection.Right,
        colors: this.colors
      })
      .clipShape(new PathShape({ commands: this.pathCommands }))
    }
    .width(this.barWidth)
    .height(this.barHeight)
    .backgroundColor(ColorUtils.alpha($r('app.color.special_black'), 0.05))
    .borderRadius(100)
    .clip(true)
    .onSizeChange((_, newValue) => {
      const width = newValue.width
      if (width) {
        const widthValue = width as number
        const tempBarWidth =
          Math.max(((this.high - this.low) / (this.maxTemp - this.minTemp)) * widthValue, this.barHeight)
        const marginLeft = ((this.low - this.minTemp) / (this.maxTemp - this.minTemp)) * widthValue
        let todayTempMarginLeft = 0
        if (this.temp) {
          todayTempMarginLeft = (this.temp - this.minTemp) / (this.maxTemp - this.minTemp) * widthValue
          if (todayTempMarginLeft < 0) {
            todayTempMarginLeft = 0
          }
          if (todayTempMarginLeft > widthValue - this.barHeight) {
            todayTempMarginLeft = widthValue - this.barHeight
          }
          if (todayTempMarginLeft < marginLeft) {
            todayTempMarginLeft = marginLeft
          }
          if (todayTempMarginLeft > marginLeft + tempBarWidth - this.barHeight) {
            todayTempMarginLeft = marginLeft + tempBarWidth - this.barHeight
          }
        }
        const colors: Array<[ResourceColor, number]> = []
        let startStop = 0
        if (this.minTemp <= 0) {
          startStop += (0 - this.minTemp) / (this.maxTemp - this.minTemp)
          colors.push([$r('app.color.color_55dffc'), startStop])
        }
        if (this.maxTemp > 0 && this.minTemp <= 15) {
          startStop += (Math.min(this.maxTemp, 15) - Math.max(this.minTemp, 0)) / (this.maxTemp - this.minTemp)
          colors.push([$r('app.color.color_eade6f'), startStop])
        }
        if (this.maxTemp > 15 && this.minTemp <= 30) {
          startStop +=
          (Math.min(this.maxTemp, 30) - Math.max(this.minTemp, 15)) / (this.maxTemp - this.minTemp)
          colors.push([$r('app.color.color_feba4f'), startStop])
        }
        if (this.maxTemp > 30) {
          colors.push([$r('app.color.color_ff6f55'), 1])
        }
        this.colors = colors
        const barHeightPx = vp2px(this.barHeight)
        const marginLeftPx = vp2px(marginLeft)
        const tempBarWidthPx = vp2px(tempBarWidth)
        const radius = barHeightPx / 2

        this.pathCommands =
          `M ${marginLeftPx + radius} 0 Q ${marginLeftPx} ${radius}, ${marginLeftPx +
            radius} ${barHeightPx} H ${marginLeftPx + tempBarWidthPx -
            radius} Q ${marginLeftPx + tempBarWidthPx} ${radius}, ${marginLeftPx + tempBarWidthPx - radius} 0 Z`
        this.todayTempMarginLeft = todayTempMarginLeft
      }
    })
  }
}

曲线状态下的气温曲线

WeatherDailyTempPanel({
  panelWidth: 68.5,
  panelHeight: 128,
  preData: this.weatherItemData?.weatherData?.forecast15?.[index - 1],
  data: item,
  nextData: this.weatherItemData?.weatherData?.forecast15?.[index + 1],
  maxTemp: this.maxTempData()?.high,
  minTemp: this.minTempData()?.low
})
@ComponentV2
export struct WeatherDailyTempPanel {
  @Param @Require panelWidth: number
  @Param @Require panelHeight: number
  @Param preData?: WeatherDetailData = undefined
  @Param data?: WeatherDetailData = undefined
  @Param nextData?: WeatherDetailData = undefined
  @Param maxTemp: number = 0
  @Param minTemp: number = 0
  private topAndBottomGaps: number = 32
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)

  build() {
    Canvas(this.canvasContext)
      .width(this.panelWidth)
      .height(this.panelHeight)
      .onReady(() => {
        this.draw()
      })
  }

  draw() {
    const canvas = this.canvasContext
    if (!canvas) {
      return
    }
    this.drawStartArea(canvas, true)
    this.drawStartArea(canvas, false)
    this.drawCenterArea(canvas, true)
    this.drawCenterArea(canvas, false)
    this.drawEndArea(canvas, true)
    this.drawEndArea(canvas, false)
  }

  drawStartArea(canvas: CanvasRenderingContext2D, isHigh: boolean) {
    if (!this.preData && this.data && this.nextData) {
      const centerX = this.panelWidth / 2
      const tempYAxis = this.calTempYAxis((isHigh ? this.data.high : this.data.low) ?? 0)
      const nextTempYAxis = this.calTempYAxis((isHigh ? this.nextData.high : this.nextData.low) ?? 0)
      canvas.lineWidth = 1
      canvas.beginPath()
      canvas.moveTo(centerX, tempYAxis)
      canvas.quadraticCurveTo(centerX + this.panelWidth / 4, (nextTempYAxis - tempYAxis) / 4 + tempYAxis,
        this.panelWidth, (nextTempYAxis - tempYAxis) / 2 + tempYAxis)
      canvas.strokeStyle = ColorUtils.alpha($r('app.color.special_white'), 0.3)
      canvas.stroke()

      canvas.beginPath()
      canvas.arc(centerX, tempYAxis, 2.5, 0, Math.PI * 2)
      canvas.fillStyle = ColorUtils.alpha($r('app.color.special_white'), 0.3)
      canvas.fill()

      this.drawTemp(canvas, getTemp(isHigh ? this.data.high : this.data.low),
        ColorUtils.alpha($r('app.color.special_white'), 0.3), tempYAxis, isHigh)
    }
  }

  drawCenterArea(canvas: CanvasRenderingContext2D, isHigh: boolean) {
    if (this.preData && this.data && this.nextData) {
      const centerX = this.panelWidth / 2
      const dateIsToday = DateUtil.isToday(getHarmonyDateTimeFormattedString(this.data.date))
      const dateIsBefore = isBefore(this.data.date)
      const tempYAxis = this.calTempYAxis((isHigh ? this.data.high : this.data.low) ?? 0)
      const preTempYAxis = this.calTempYAxis((isHigh ? this.preData.high : this.preData.low) ?? 0)
      const nextTempYAxis = this.calTempYAxis((isHigh ? this.nextData.high : this.nextData.low) ?? 0)
      const moveToY = (preTempYAxis - tempYAxis) / 2 + tempYAxis
      const p1 = { x: this.panelWidth / 4, y: (moveToY - tempYAxis) / 2 + tempYAxis } as Position
      const p2 =
        { x: this.panelWidth - this.panelWidth / 4, y: (nextTempYAxis - tempYAxis) / 4 + tempYAxis } as Position
      if (dateIsToday || dateIsBefore) {
        canvas.save()
        canvas.rect(this.panelWidth / 2, 0, this.panelWidth / 2, this.panelHeight)
        canvas.clip()
        this.drawCenterLine(canvas, moveToY, p1, p2, tempYAxis, nextTempYAxis,
          ColorUtils.alpha($r('app.color.special_white'), 1))
        canvas.restore()

        canvas.save()
        canvas.rect(0, 0, this.panelWidth / 2, this.panelHeight)
        canvas.clip()
        this.drawCenterLine(canvas, moveToY, p1, p2, tempYAxis, nextTempYAxis,
          ColorUtils.alpha($r('app.color.special_white'), 0.3))
        canvas.restore()
      } else {
        this.drawCenterLine(canvas, moveToY, p1, p2, tempYAxis, nextTempYAxis,
          ColorUtils.alpha($r('app.color.special_white'), 1))
      }

      canvas.beginPath()
      canvas.arc(centerX, (p2.y - p1.y) / 2 + p1.y, 2.5, 0, Math.PI * 2)
      canvas.fillStyle = ColorUtils.alpha($r('app.color.special_white'), dateIsBefore ? 0.3 : 1)
      canvas.fill()

      this.drawTemp(canvas, getTemp(isHigh ? this.data.high : this.data.low),
        ColorUtils.alpha($r('app.color.special_white'), dateIsBefore ? 0.3 : 1), (p2.y - p1.y) / 2 + p1.y, isHigh)
    }
  }

  drawCenterLine(canvas: CanvasRenderingContext2D, moveToY: number, p1: Position, p2: Position,
    tempYAxis: number, nextTempYAxis: number, lineColor: number) {
    if (this.preData && this.data && this.nextData) {
      canvas.lineWidth = 1
      canvas.beginPath()
      canvas.moveTo(0, moveToY)
      canvas.bezierCurveTo(p1.x, p2.y, p2.x, p2.y, this.panelWidth, (nextTempYAxis - tempYAxis) / 2 + tempYAxis)
      canvas.strokeStyle = lineColor
      canvas.stroke()
    }
  }

  drawEndArea(canvas: CanvasRenderingContext2D, isHigh: boolean) {
    if (this.preData && this.data && !this.nextData) {
      const centerX = this.panelWidth / 2
      const tempYAxis = this.calTempYAxis((isHigh ? this.data.high : this.data.low) ?? 0)
      const preTempYAxis = this.calTempYAxis((isHigh ? this.preData.high : this.preData.low) ?? 0)
      canvas.lineWidth = 1
      canvas.beginPath()
      canvas.moveTo(centerX, tempYAxis)
      canvas.quadraticCurveTo(centerX - this.panelWidth / 4, (preTempYAxis - tempYAxis) / 4 + tempYAxis,
        0, (preTempYAxis - tempYAxis) / 2 + tempYAxis)
      canvas.strokeStyle = ColorUtils.alpha($r('app.color.special_white'), 1)
      canvas.stroke()

      canvas.beginPath()
      canvas.arc(centerX, tempYAxis, 2.5, 0, Math.PI * 2)
      canvas.fillStyle = ColorUtils.alpha($r('app.color.special_white'), 1)
      canvas.fill()

      this.drawTemp(canvas, getTemp(isHigh ? this.data.high : this.data.low),
        ColorUtils.alpha($r('app.color.special_white'), 1), tempYAxis, isHigh)
    }
  }

  drawTemp(canvas: CanvasRenderingContext2D, text: string, textColor: number, tempYAxis: number, isHigh: boolean) {
    const centerX = this.panelWidth / 2
    canvas.textAlign = 'center'
    canvas.font = '13vp sans-serif'
    canvas.fillStyle = textColor
    canvas.textBaseline = isHigh ? 'bottom' : 'top'
    canvas.fillText(text, centerX, isHigh ? tempYAxis - 4 : tempYAxis + 4)
  }

  calTempYAxis(temp: number): number {
    const diff = this.maxTemp - temp
    const diffTemp = this.maxTemp - this.minTemp
    const percent = diff / diffTemp
    return (this.panelHeight - 2 * this.topAndBottomGaps) * percent + this.topAndBottomGaps
  }
}