从零开始纯血鸿蒙天气预报-天气预览功能

144 阅读1分钟

易得天气

1、添加城市预览功能

2、天气卡片浏览功能

效果图

2025-06-20 17.56.28.gif

2025-06-20 17.59.23.gif

天气预览页面

@Route({ name: RouterConstants.WEATHER_PREVIEW_PAGE })
@ComponentV2
export struct WeatherPreviewPage {
  @Local previewCityData: CityData = ZRouter.getInstance().getParamByKey<CityData>(Constants.CITY_DATA)
  @Local weatherPreviewVM: WeatherPreviewViewModel = new WeatherPreviewViewModel()

  aboutToAppear(): void {
    const weatherData = AppRuntimeData.getInstance().currentCityData?.weatherData
    this.weatherPreviewVM.generateWeatherBg(undefined, weatherData?.weatherType, weatherData?.sunrise,
      weatherData?.sunset)
    this.weatherPreviewVM.obtainWeatherData(this.previewCityData.cityid ?? '')
  }

  @Builder
  functionButton(action: string, block: () => void) {
    Text(action)
      .textAlign(TextAlign.Center)
      .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 })
      .fontColor($r('app.color.special_white'))
      .fontSize(14)
      .padding({ left: 16, right: 16 })
      .backgroundColor(ColorUtils.alpha(this.weatherPreviewVM.isDark ? $r('app.color.special_white') :
      $r('app.color.special_black'), this.weatherPreviewVM.panelOpacity))
      .borderRadius(100)
      .height(32)
      .onClick(block)
  }

  build() {
    NavDestination() {
      Stack({ alignContent: Alignment.Top }) {
        Stack({ alignContent: Alignment.Top }) {
          MultipleStatusLayout({
            message: this.weatherPreviewVM.errorMessage,
            viewState: this.weatherPreviewVM.viewState,
            loadingProgressColor: $r('app.color.special_white'),
            contentView: () => {
              this.weatherContentList()
            }
          })
          Row() {
            this.functionButton('取消', () => {
              ZRouter.getInstance().pop()
            })
            this.functionButton('添加', () => {
              ZRouter.getInstance().popWithResult(this.previewCityData)
            })
          }
          .width('100%')
          .padding({ left: 12, top: px2vp(AppUtil.getStatusBarHeight()) + 12, right: 12 })
          .justifyContent(FlexAlign.SpaceBetween)
        }
        .width('100%')
        .height('100%')
        .opacity(this.weatherPreviewVM.contentOpacity)
        .animation({ duration: 200, curve: Curve.Linear })
      }
      .width('100%')
      .height('100%')
      .linearGradient({
        direction: GradientDirection.Bottom,
        colors: this.weatherPreviewVM.weatherBg
      })
    }
    .hideTitleBar(true)
    .height('100%')
    .width('100%')
    .onReady((context) => {
      ZRouter.animateMgr()
        .registerAnimParam(this, context)
        .setEnterAnimate({ duration: 500, curve: Curve.LinearOutSlowIn })
        .setExitAnimate({ duration: 500, curve: Curve.LinearOutSlowIn })
        .addAnimateOptions(new TranslateAnimationOptions({ y: '100%' }))
    })
    .onDisAppear(() => {
      ZRouter.animateMgr().unregisterAnim(this)
    })
    .attributeModifier(ZRouter.animateMgr().modifier(this))
    .backgroundColor($r('app.color.bg_color'))
  }

  @Builder
  weatherContentList() {
    Stack({ alignContent: Alignment.Top }) {
      List({ scroller: this.weatherPreviewVM.listScroller }) {
        ForEach(this.weatherPreviewVM.weatherItemsFilter, (item: WeatherItemData) => {
          if (item.itemType == Constants.ITEM_TYPE_ALARMS) {
            WeatherAlarmsPanel({
              weatherItemData: item,
              isDark: this.weatherPreviewVM.isDark,
              panelOpacity: this.weatherPreviewVM.panelOpacity,
              showHideWeatherContent: (show) => {
                this.weatherPreviewVM.showHideWeatherContent(show)
              }
            })
          } else if (item.itemType == Constants.ITEM_TYPE_AIR_QUALITY) {
            WeatherAirQualityPanel({
              weatherItemData: item,
              isDark: this.weatherPreviewVM.isDark,
              panelOpacity: this.weatherPreviewVM.panelOpacity,
              showHideWeatherContent: (show) => {
                this.weatherPreviewVM.showHideWeatherContent(show)
              }
            })
          } else if (item.itemType == Constants.ITEM_TYPE_HOUR_WEATHER) {
            WeatherHourPanel({
              weatherItemData: item,
              isDark: this.weatherPreviewVM.isDark,
              panelOpacity: this.weatherPreviewVM.panelOpacity
            })
          } else if (item.itemType == Constants.ITEM_TYPE_DAILY_WEATHER) {
            WeatherDailyPanel({
              weatherItemData: item,
              isDark: this.weatherPreviewVM.isDark,
              panelOpacity: this.weatherPreviewVM.panelOpacity
            })
          } else if (item.itemType == Constants.ITEM_TYPE_OBSERVE) {
            WeatherObservePanel({
              weatherItemData: item,
              itemTypeObserves: this.weatherPreviewVM.itemTypeObserves,
              isDark: this.weatherPreviewVM.isDark,
              isWeatherHeaderDark: this.weatherPreviewVM.isWeatherHeaderDark,
              panelOpacity: this.weatherPreviewVM.panelOpacity,
              showHideWeatherContent: (show) => {
                this.weatherPreviewVM.showHideWeatherContent(show)
              }
            })
          } else if (item.itemType == Constants.ITEM_TYPE_LIFE_INDEX) {
            WeatherLifeIndexPanel({
              weatherItemData: item,
              isDark: this.weatherPreviewVM.isDark,
              panelOpacity: this.weatherPreviewVM.panelOpacity
            })
          }
        }, (item: WeatherItemData) => item.itemType.toString())
        if (StrUtil.isNotEmpty(this.source)) {
          this.footer()
        }
      }
      .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.weatherPreviewVM.listScroller.currentOffset().yOffset
        this.weatherPreviewVM.setListOffset(yOffset +
          (Constants.WEATHER_HEADER_MAX_HEIGHT - Constants.WEATHER_HEADER_MIN_HEIGHT))
      })

      WeatherHeaderWidget({
        isWeatherHeaderDark: this.weatherPreviewVM.isWeatherHeaderDark,
        weatherItemData: this.weatherPreviewVM.weatherHeaderItemData,
        weatherHeaderOffset: this.weatherPreviewVM.listOffset
      })
    }
    .width('100%')
    .height('100%')
    .padding({ left: Constants.ITEM_PANEL_MARGIN, right: Constants.ITEM_PANEL_MARGIN })
  }

  get source() {
    return this.weatherPreviewVM.weatherItemsFilter?.[0].weatherData?.source?.title
  }

  @Builder
  footer() {
    ListItem() {
      Column() {
        Text(`天气信息来自${this.source}`)
          .fontSize(12)
          .fontColor(ColorUtils.alpha(this.weatherPreviewVM.isDark ? $r('app.color.special_white') :
          $r('app.color.special_black'), 0.4))
      }
      .width('100%')
      .alignItems(HorizontalAlign.Center)
      .margin({ bottom: px2vp(AppUtil.getNavigationIndicatorHeight()) + 8 })
    }
  }
}

城市卡片选择页面

@Builder
export function WeatherCitySelectorBuilder(options: WeatherCitySelectorOptions) {
  WeatherCitySelector({ options: options })
}

@ComponentV2
export struct WeatherCitySelector {
  @Require @Param options: WeatherCitySelectorOptions
  @Local isSwiperShow: boolean = false
  @Local hideSwiper: boolean = false
  @Local blurAnimValue: number = 0
  @Local list?: Array<Pair<CityData | undefined, Array<WeatherItemData> | undefined>>
  @Local scaleArr: Array<number> = []
  @Local currentCityData?: CityData
  @Local currentData?: Array<WeatherItemData>
  @Local scaleAnimValue: number = 0.6
  private swiperController: SwiperController = new SwiperController()
  private screenWidth = px2vp(DisplayUtil.getWidth())
  private screenHeight = px2vp(DisplayUtil.getHeight())
  private index = 0

  aboutToAppear(): void {
    this.options.ref.exit = this.exit
    setTimeout(() => {
      this.blurAnimValue = 1
    }, 16)
    this.generateData()
      .then((list) => {
        this.list = list
        const scaleArr = list.map((e) => {
          return e.first?.cityid == AppRuntimeData.getInstance().currentCityData?.cityid ? 1 : 0.9
        })
        this.scaleArr = scaleArr
        this.isSwiperShow = true
        const index = scaleArr.findIndex((e) => e == 1)
        if (index > 0) {
          this.getUIContext().postFrameCallback(new MyFrameCallback({
            onIdleCallback: () => {
              this.swiperController.changeIndex(index)
            }
          }))
        }
      })
  }

  async generateData(): Promise<Array<Pair<CityData | undefined, Array<WeatherItemData> | undefined>>> {
    const list: Array<Pair<CityData | undefined, Array<WeatherItemData> | undefined>> = []
    const currentCityIdList = await PreferencesUtil.get(Constants.CURRENT_CITY_ID_LIST, []) as Array<string>
    const query = await appDatabase.cityDataDao.query()
    currentCityIdList.forEach((cityId) => {
      const cityData = query.find(it => it.key == cityId)
      const findCityId = cityData?.cityid ?? ''
      if (StrUtil.isNotEmpty(findCityId)) {
        const pair = {
          first: cityData,
          second: WeatherDataUtils.generateWeatherItems(AppRuntimeData.getInstance().currentWeatherCardSort,
            AppRuntimeData.getInstance().currentWeatherObservesCardSort,
            AppRuntimeData.getInstance().getWeatherData(cityId))
        } as Pair<CityData | undefined, Array<WeatherItemData> | undefined>
        if (cityId == Constants.LOCATION_CITY_ID) {
          list.splice(0, 0, pair)
        } else {
          list.push(pair)
        }
      }
    })
    return list
  }

  exit = () => {
    this.isSwiperShow = false
    setTimeout(() => {
      this.blurAnimValue = 0
      setTimeout(() => {
        DialogHelper.closeDialog('weather_city_selector')
      }, 200)
    }, 200)
  }

  build() {
    Stack() {
      Stack()
        .width('100%')
        .height('100%')
        .backgroundColor(ColorUtils.alpha($r('app.color.special_black'), 0.15))
        .opacity(this.blurAnimValue)
        .blur(this.blurAnimValue * 100)
        .animation({ curve: Curve.Linear, duration: 200 })
        .onClick(() => {
          this.exit()
        })
      Stack({ alignContent: Alignment.Bottom }) {
        this.changeWeatherBgButton()
      }
      .hitTestBehavior(HitTestMode.Transparent)
      .width('100%')
      .height('100%')

      this.content()

      if (!ObjectUtil.isNull(this.currentCityData)) {
        WeatherCitySnapshot({
          cityData: this.currentCityData,
          data: this.currentData,
          scaleParams: { x: this.scaleAnimValue, y: this.scaleAnimValue }
        })
      }
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  content() {
    Swiper(this.swiperController) {
      ForEach(this.list, (item: Pair<CityData | undefined, Array<WeatherItemData> | undefined>, index: number) => {
        WeatherCitySelectorItem({
          pair: item, onTap: () => {
            if (this.index == index) {
              this.switchWeatherCity(index)
            } else {
              this.swiperController.changeIndex(index, true)
            }
          }
        })
          .scale({ x: this.scaleArr[index], y: this.scaleArr[index] })
      }, (item: Pair<CityData | undefined, Array<WeatherItemData> | undefined>) => item.first?.key)
    }
    .direction(Direction.Rtl)
    .clip(false)
    .loop(false)
    .prevMargin(this.screenWidth * 0.2)
    .nextMargin(this.screenWidth * 0.2)
    .indicator(false)
    .customContentTransition({
      timeout: 200,
      transition: (proxy: SwiperContentTransitionProxy) => {
        let scale = 1.0 - (Math.abs(proxy.position)) * 0.1
        this.scaleArr[(proxy.index)%(this.scaleArr.length)] = scale
      }
    })
    .onChange((index) => {
      this.index = index
    })
    .curve(Curve.Friction)
    .height(this.screenHeight * 0.6)
    .opacity(this.hideSwiper ? 0 : (this.isSwiperShow ? 1 : 0))
    .translate({ x: this.isSwiperShow ? 0 : -this.screenWidth })
    .animation({ curve: curves.interpolatingSpring(14, 1, 170, 17), duration: 200 })
    .onClick(() => {
      this.exit()
    })
  }

  @Builder
  changeWeatherBgButton() {
    Text('更改天气背景')
      .width(172)
      .height(42)
      .textAlign(TextAlign.Center)
      .fontSize(16)
      .fontColor($r('app.color.special_white'))
      .borderRadius(100)
      .borderWidth(0.5)
      .borderColor(ColorUtils.alpha($r('app.color.special_white'), 0.5))
      .backgroundColor(ColorUtils.alpha($r('app.color.special_black'), 0.2))
      .margin({ bottom: px2vp(AppUtil.getNavigationIndicatorHeight()) + 32 })
      .opacity(this.blurAnimValue)
      .animation({ curve: Curve.Linear, duration: 200 })
      .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 })
      .onClick(() => {
      })
  }

  switchWeatherCity(index: number) {
    this.currentCityData = this.list?.[index].first
    this.currentData = this.list?.[index].second
    this.getUIContext().postFrameCallback(new MyFrameCallback({
      onIdleCallback: () => {
        this.scaleAnimValue = 1
        this.hideSwiper = true
        setTimeout(() => {
          this.exit()
        }, 400)
      }
    }))
  }
}

下一个功能:更改天气背景功能