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

易得天气

天气预报主界面,目前完成了头部UI、极端天气面板UI、空气质量面板UI、每小时天气面板UI

目前界面上sticky效果是通过监听页面位置变化从而实现header的偏移,不过不知道是不是因为模拟器的原因,导致header会有抖动,也没有真机进行测试,只能等真机测试,如果还是有抖动,只能换个方案实现

效果图

页面布局

scss 复制代码
@Builder
weatherContentList() {
  Stack({ alignContent: Alignment.Top }) {
    List({ scroller: this.weatherMainVM.listScroller }) {
      ForEach(this.weatherMainVM.weatherItemsFilter, (item: WeatherItemData) => {
        if (item.itemType == Constants.ITEM_TYPE_ALARMS) {
          WeatherAlarmsPanel({
            weatherItemData: item,
            isDark: this.weatherMainVM.isDark,
            panelOpacity: this.weatherMainVM.panelOpacity
          })
        } else if (item.itemType == Constants.ITEM_TYPE_AIR_QUALITY) {
          WeatherAirQualityPanel({
            weatherItemData: item,
            isDark: this.weatherMainVM.isDark,
            panelOpacity: this.weatherMainVM.panelOpacity
          })
        } else if (item.itemType == Constants.ITEM_TYPE_HOUR_WEATHER) {
          WeatherHourPanel({
            weatherItemData: item,
            isDark: this.weatherMainVM.isDark,
            panelOpacity: this.weatherMainVM.panelOpacity
          })
        } else {
          ListItem() {
            Text(item.itemType.toString())
              .width("100%")
              .height(800)
              .fontSize(20)
              .textAlign(TextAlign.Center)
              .backgroundColor(Color.Gray)
          }
        }
      }, (item: WeatherItemData) => item.itemType.toString())
    }
    .width('100%')
    .height(`calc(100% - ${px2vp(AppUtil.getStatusBarHeight()) + Constants.WEATHER_HEADER_MIN_HEIGHT}vp)`)
    .scrollBar(BarState.Off)
    .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
    .divider({ strokeWidth: 12, color: $r('app.color.transparent') })
    .margin({ top: px2vp(AppUtil.getStatusBarHeight()) + Constants.WEATHER_HEADER_MIN_HEIGHT })
    .contentStartOffset(Constants.WEATHER_HEADER_MAX_HEIGHT - Constants.WEATHER_HEADER_MIN_HEIGHT)
    .borderRadius({ topLeft: Constants.ITEM_PANEL_RADIUS, topRight: Constants.ITEM_PANEL_RADIUS })
    .clip(true)
    .onDidScroll(() => {
      const yOffset = this.weatherMainVM.listScroller.currentOffset().yOffset
      this.weatherMainVM.setListOffset(yOffset +
        (Constants.WEATHER_HEADER_MAX_HEIGHT - Constants.WEATHER_HEADER_MIN_HEIGHT))
    })

    WeatherHeaderWidget({
      isWeatherHeaderDark: this.weatherMainVM.isWeatherHeaderDark,
      weatherItemData: this.weatherMainVM.weatherHeaderItemData,
      weatherHeaderOffset: this.weatherMainVM.listOffset
    })
  }
  .width('100%')
  .height('100%')
  .padding({ left: Constants.ITEM_PANEL_MARGIN, right: Constants.ITEM_PANEL_MARGIN })
  .opacity(this.weatherMainVM.isShowCityManagerPage ? fixPercent((0.2 - this.weatherMainVM.animValue) / 0.2) :
    1 - fixPercent((this.weatherMainVM.animValue - 0.8) / 0.2))
}

极端天气面板UI

在onAreaChange方法进行sticky的效果实现

less 复制代码
@ComponentV2
export struct WeatherAlarmsPanel {
  @Param weatherItemData?: WeatherItemData = undefined
  @Param isDark: boolean = false
  @Param panelOpacity: number = 0.1
  @Local contentOpacity: number = 1
  @Local stickyTranslateY: number = 0
  @Local titleOpacity: number = 1
  @Local timeOpacity: number = 1
  @Local panelPathCommands: string = ''

  aboutToAppear(): void {
    this.panelPathCommands = buildPathCommands(Constants.WEATHER_ALARM_PANEL_HEIGHT)
  }

  build() {
    ListItem() {
      Stack({ alignContent: Alignment.TopStart }) {
        Text('极端天气')
          .fontSize(12)
          .fontColor(ColorUtils.alpha($r('app.color.special_white'), 0.6))
          .width('100%')
          .height(Constants.ITEM_STICKY_HEIGHT)
          .padding({ left: Constants.ITEM_PANEL_MARGIN })
          .opacity(1 - this.timeOpacity)
          .translate({ y: this.stickyTranslateY })

        Swiper() {
          ForEach(this.weatherItemData?.weatherData?.alarms, (item: WeatherAlarmsData) => {
            this.alarmItem(item)
          }, (item: WeatherAlarmsData) => item.short_title)
        }
        .width('100%')
        .height(Constants.WEATHER_ALARM_PANEL_HEIGHT)
        .indicator(
          (this.weatherItemData?.weatherData?.alarms?.length ?? 0) <= 1 ? false :
          Indicator.dot()
            .itemWidth(3)
            .itemHeight(2)
            .selectedItemWidth(6)
            .selectedItemHeight(2)
            .color(ColorUtils.alpha($r('app.color.special_white'), 0.5))
            .selectedColor($r('app.color.special_white'))
        )
        .clipShape(new PathShape({ commands: this.panelPathCommands }))
      }
      .width('100%')
      .height(Constants.WEATHER_ALARM_PANEL_HEIGHT)
      .backgroundColor(ColorUtils.alpha(this.isDark ? $r('app.color.special_white') : $r('app.color.special_black'),
        this.panelOpacity))
      .borderRadius(Constants.ITEM_PANEL_RADIUS)
      .opacity(this.contentOpacity)
      .onAreaChange((_, newValue) => {
        const y = newValue.globalPosition.y
        if (y) {
          const yValue = y as number
          const offset = px2vp(AppUtil.getStatusBarHeight()) + Constants.WEATHER_HEADER_MIN_HEIGHT - yValue
          const percent = fixPercent((offset - (Constants.WEATHER_ALARM_PANEL_HEIGHT - Constants.ITEM_STICKY_HEIGHT)) /
          Constants.ITEM_STICKY_HEIGHT)
          const stickyTranslateY =
            offset < 0 ? 0 : offset > Constants.WEATHER_ALARM_PANEL_HEIGHT - Constants.ITEM_STICKY_HEIGHT ?
              Constants.WEATHER_ALARM_PANEL_HEIGHT - Constants.ITEM_STICKY_HEIGHT : offset
          this.stickyTranslateY = stickyTranslateY + percent * Constants.ITEM_STICKY_HEIGHT * 0.5
          this.titleOpacity = fixPercent(1 - offset / 12)
          this.timeOpacity = fixPercent(1 - offset / 28)
          this.panelPathCommands = buildPathCommands(Constants.WEATHER_ALARM_PANEL_HEIGHT,
            offset + Constants.ITEM_STICKY_HEIGHT * fixPercent(offset / Constants.ITEM_STICKY_HEIGHT))
          // console.log('yValue = ' + offset + ' percent = ' + percent)
          this.contentOpacity = 1 - percent
        }
      })
    }
  }

  @Builder
  alarmItem(item: WeatherAlarmsData) {
    Column() {
      Blank().height(12)
      Text(item.short_title)
        .fontSize(16)
        .fontColor($r('app.color.special_white'))
        .fontWeight(FontWeight.Bold)
        .opacity(this.titleOpacity)
      Blank().height(6)
      Text(this.pubTimeDesc(item))
        .fontSize(13)
        .fontColor($r('app.color.special_white'))
        .opacity(this.timeOpacity)
      Blank().height(8)
      Text(item.desc)
        .fontSize(14)
        .fontColor($r('app.color.special_white'))
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
    .padding({ left: 16, right: 16 })
  }

  pubTimeDesc(item: WeatherAlarmsData): string {
    let pubTimeDesc = ''
    const date = DateUtil.getFormatDate(item.pub_time)
    const diff = DateUtil.getToday().getTime() - date.getTime()
    const fewHours = Math.floor(diff / 1000 / 60 / 60)
    pubTimeDesc = `${fewHours}小时前更新`
    if (fewHours <= 0) {
      const fewMinutes = Math.floor(diff / 1000 / 60)
      pubTimeDesc = `${fewMinutes}分钟前更新`
      if (fewMinutes <= 0) {
        const fewMills = Math.floor(diff / 1000)
        pubTimeDesc = `${fewMills}秒前更新`
      }
    }
    return pubTimeDesc
  }
}

空气质量面板UI

less 复制代码
@ComponentV2
export struct WeatherAirQualityPanel {
  @Param weatherItemData?: WeatherItemData = undefined
  @Param isDark: boolean = false
  @Param panelOpacity: number = 0.1
  @Local stickyTranslateY: number = 0
  @Local contentOpacity: number = 1
  @Local titleOpacity: number = 1
  @Local panelPathCommands: string = ''

  aboutToAppear(): void {
    this.panelPathCommands = buildPathCommands(Constants.WEATHER_AIR_QUALITY_PANEL_HEIGHT)
  }

  build() {
    ListItem() {
      Stack({ alignContent: Alignment.TopStart }) {
        Text('极端天气')
          .fontSize(12)
          .fontColor(ColorUtils.alpha($r('app.color.special_white'), 0.6))
          .width('100%')
          .height(Constants.ITEM_STICKY_HEIGHT)
          .padding({ left: Constants.ITEM_PANEL_MARGIN })
          .opacity(1 - this.titleOpacity)
          .translate({ y: this.stickyTranslateY })

        Column() {
          Blank().height(12)
          Row() {
            Text(this.title)
              .fontSize(16)
              .fontColor($r('app.color.special_white'))
              .fontWeight(FontWeight.Bold)
            Stack() {
              Image($r('app.media.ic_query_icon'))
                .width(14)
                .height(14)
                .draggable(false)
                .colorFilter(ColorUtils.translateColor($r('app.color.special_white')))
            }
            .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 })
            .padding({
              left: 12,
              top: 4,
              right: 12,
              bottom: 4
            })
            .onClick(() => {
            })
          }
          .opacity(this.titleOpacity)

          Blank().height(12)
          AirQualityBar({ barWidth: '100%', barHeight: 4, aqi: this.weatherItemData?.weatherData?.evn?.aqi ?? 0 })
          Blank().height(10)
          Text(this.desc)
            .fontSize(13)
            .fontColor($r('app.color.special_white'))
        }
        .width('100%')
        .height(Constants.WEATHER_AIR_QUALITY_PANEL_HEIGHT)
        .alignItems(HorizontalAlign.Start)
        .padding({ left: 16, right: 16 })
        .clipShape(new PathShape({ commands: this.panelPathCommands }))
      }
      .width('100%')
      .height(Constants.WEATHER_AIR_QUALITY_PANEL_HEIGHT)
      .backgroundColor(ColorUtils.alpha(this.isDark ? $r('app.color.special_white') : $r('app.color.special_black'),
        this.panelOpacity))
      .borderRadius(Constants.ITEM_PANEL_RADIUS)
      .opacity(this.contentOpacity)
      .onAreaChange((_, newValue) => {
        const y = newValue.globalPosition.y
        if (y) {
          const yValue = y as number
          const offset = px2vp(AppUtil.getStatusBarHeight()) + Constants.WEATHER_HEADER_MIN_HEIGHT - yValue
          const percent =
            fixPercent((offset - (Constants.WEATHER_AIR_QUALITY_PANEL_HEIGHT - Constants.ITEM_STICKY_HEIGHT)) /
            Constants.ITEM_STICKY_HEIGHT)
          const stickyTranslateY =
            offset < 0 ? 0 : offset > Constants.WEATHER_AIR_QUALITY_PANEL_HEIGHT - Constants.ITEM_STICKY_HEIGHT ?
              Constants.WEATHER_AIR_QUALITY_PANEL_HEIGHT - Constants.ITEM_STICKY_HEIGHT : offset
          this.stickyTranslateY = stickyTranslateY + percent * Constants.ITEM_STICKY_HEIGHT * 0.5
          this.titleOpacity = fixPercent(1 - offset / 12)
          this.panelPathCommands = buildPathCommands(Constants.WEATHER_AIR_QUALITY_PANEL_HEIGHT,
            offset + Constants.ITEM_STICKY_HEIGHT * fixPercent(offset / Constants.ITEM_STICKY_HEIGHT))
          // console.log('yValue = ' + offset + ' percent = ' + percent)
          this.contentOpacity = 1 - percent
        }
      })
    }
  }

  get title() {
    return `${this.weatherItemData?.weatherData?.evn?.aqi} - ${this.weatherItemData?.weatherData?.evn?.aqi_level_name}`
  }

  get desc() {
    return `当前AQI为${this.weatherItemData?.weatherData?.evn?.aqi}`
  }
}

每小时天气预报面板UI

less 复制代码
@ComponentV2
export struct WeatherHourPanel {
  @Param weatherItemData?: WeatherItemData = undefined
  @Param isDark: boolean = false
  @Param panelOpacity: number = 0.1
  @Local contentOpacity: number = 1
  @Local stickyTranslateY: number = 0
  @Local panelPathCommands: string = ''

  aboutToAppear(): void {
    this.panelPathCommands = buildPathCommands(Constants.WEATHER_HOUR_PANEL_HEIGHT)
  }

  build() {
    ListItem() {
      Stack({ alignContent: Alignment.TopStart }) {
        Text('每小时天气预报')
          .fontSize(12)
          .fontColor(ColorUtils.alpha($r('app.color.special_white'), 0.6))
          .width('100%')
          .height(Constants.ITEM_STICKY_HEIGHT)
          .padding({ left: Constants.ITEM_PANEL_MARGIN })
          .translate({ y: this.stickyTranslateY })

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

          List() {
            ForEach(this.weatherItemData?.weatherData?.hourfc, (item: WeatherHourData) => {
              this.hourItem(item)
            }, (item: WeatherHourData) => item.time)
          }
          .width('100%')
          .height('100%')
          .layoutWeight(1)
          .scrollBar(BarState.Off)
          .edgeEffect(EdgeEffect.None)
          .listDirection(Axis.Horizontal)
          .contentStartOffset(16)
          .contentEndOffset(16)
          .divider({ strokeWidth: 28, color: $r('app.color.transparent') })
        }
        .padding({ top: Constants.ITEM_STICKY_HEIGHT })
        .clipShape(new PathShape({ commands: this.panelPathCommands }))
      }
      .width('100%')
      .height(Constants.WEATHER_HOUR_PANEL_HEIGHT)
      .backgroundColor(ColorUtils.alpha(this.isDark ? $r('app.color.special_white') : $r('app.color.special_black'),
        this.panelOpacity))
      .borderRadius(Constants.ITEM_PANEL_RADIUS)
      .opacity(this.contentOpacity)
      .onAreaChange((_, newValue) => {
        const y = newValue.globalPosition.y
        if (y) {
          const yValue = y as number
          const offset = px2vp(AppUtil.getStatusBarHeight()) + Constants.WEATHER_HEADER_MIN_HEIGHT - yValue
          const percent = fixPercent((offset - (Constants.WEATHER_HOUR_PANEL_HEIGHT - Constants.ITEM_STICKY_HEIGHT)) /
          Constants.ITEM_STICKY_HEIGHT)
          const stickyTranslateY =
            offset < 0 ? 0 : offset > Constants.WEATHER_HOUR_PANEL_HEIGHT - Constants.ITEM_STICKY_HEIGHT ?
              Constants.WEATHER_HOUR_PANEL_HEIGHT - Constants.ITEM_STICKY_HEIGHT : offset
          this.stickyTranslateY = stickyTranslateY + percent * Constants.ITEM_STICKY_HEIGHT * 0.5
          this.panelPathCommands =
            buildPathCommands(Constants.WEATHER_HOUR_PANEL_HEIGHT, offset + Constants.ITEM_STICKY_HEIGHT)
          this.contentOpacity = 1 - percent
        }
      })
    }
  }

  @Builder
  hourItem(item: WeatherHourData) {
    ListItem() {
      Column() {
        Blank().height(12)
        Text(this.isSunrise(item) ? item.sunriseAndSunset?.sunrise :
          this.isSunset(item) ? item.sunriseAndSunset?.sunset :
          getWeatherHourTime(item?.time, this.currentWeatherDetailData?.sunrise, this.currentWeatherDetailData?.sunset))
          .fontSize(14)
          .fontColor($r('app.color.special_white'))
        Blank().height(12)
        Image(this.isSunrise(item) ? $r('app.media.ic_sunrise_icon') :
          this.isSunset(item) ? $r('app.media.ic_sunset_icon') :
          WeatherIconUtils.getWeatherIconByType(item.type ?? -1, item.third_type ?? '', false))
          .width(24)
          .height(24)
        Blank().height(12)
        Text(this.isSunrise(item) ? '日出' : this.isSunset(item) ? '日落' : getTemp(item.wthr))
          .fontSize(14)
          .fontColor($r('app.color.special_white'))
      }
    }
  }

  get currentWeatherDetailData() {
    return this.weatherItemData?.weatherData?.forecast15?.find(it => StrUtil.equal(it.date,
      DateUtil.getFormatDateStr(DateUtil.getToday(), Constants.YYYYMMDD)))
  }

  isSunrise(item: WeatherHourData): boolean {
    const sunriseAndSunset = item.sunriseAndSunset
    if (sunriseAndSunset) {
      return StrUtil.isNotEmpty(sunriseAndSunset.sunrise)
    }
    return false
  }

  isSunset(item: WeatherHourData): boolean {
    const sunriseAndSunset = item.sunriseAndSunset
    if (sunriseAndSunset) {
      return StrUtil.isNotEmpty(sunriseAndSunset.sunset)
    }
    return false
  }
}
相关推荐
zhousg2 小时前
HarmonyOS NEXT 实战系列02-布局基础
harmonyos
zhousg2 小时前
HarmonyOS NEXT 实战系列01-ArkTS基础
harmonyos
别说我什么都不会2 小时前
鸿蒙(HarmonyOS)性能优化实战-性能测试工具SmartPerf Editor
性能优化·harmonyos
趋势大仙4 小时前
harmony Next 基础知识点1
开发语言·华为·harmonyos
__Benco4 小时前
OpenHarmony子系统开发 - 模块配置规则
人工智能·harmonyos
轻口味5 小时前
【每日学点HarmonyOS Next知识】类型判断、刘海高度、隐私弹窗、滑动下一页效果、清楚缓存
缓存·华为·harmonyos·harmonyosnext
苏杰豪6 小时前
鸿蒙特效教程01-哔哩哔哩点赞与一键三连效果实现教程
华为·harmonyos
全栈若城6 小时前
38.HarmonyOS NEXT Layout布局组件系统详解(五):对齐方式设置
harmonyos
全栈若城6 小时前
44.HarmonyOS NEXT Layout布局组件系统详解(十一):最佳实践与高级应用
harmonyos