从零开始纯血鸿蒙天气预报-天气卡片排序

106 阅读3分钟

易得天气

1、天气卡片排序

2、天气专业数据卡片排序

效果图

2025-04-07 20.27.50.gif

2025-04-07 20.30.30.gif

卡片排序页面

@Route({ name: RouterConstants.WEATHER_CARD_SORT_PAGE })
@ComponentV2
export struct WeatherCardSortPage {
  @Local isBackEnabled: boolean = true
  @Local currentWeatherCardSort: Array<number> = []
  @Local currentWeatherObserveCardSort: Array<number> = []
  @Local contentOpacity: number = 1
  @Local gridOpacity: number = 0
  @Local marginTop: number = 0
  @Local animDuration: number = 0
  private tempMarginTop = 0

  aboutToAppear(): void {
    this.currentWeatherCardSort =
      AppRuntimeData.getInstance().currentWeatherCardSort.filter(it => it != Constants.ITEM_TYPE_WEATHER_HEADER)
    this.currentWeatherObserveCardSort = AppRuntimeData.getInstance().currentWeatherObservesCardSort
  }

  build() {
    NavDestination() {
      Column() {
        CommonTitleBar({
          statusBarColor: $r('app.color.transparent'),
          titleBarColor: $r('app.color.transparent'),
          showBottomLine: false,
          leftType: TitleType.CUSTOM,
          leftCustomView: () => {
            this.leftIcon()
          },
          centerText: '卡片排序',
          centerTextColor: $r('app.color.black'),
          rightType: TitleType.TEXT,
          rightText: '恢复默认',
          rightTextColor: ColorUtils.alpha($r('app.color.black'), this.isBackEnabled ? 1 : 0.2),
          rightOnClick: this.isBackEnabled ? () => {
            const currentWeatherCardSort = [Constants.ITEM_TYPE_WEATHER_HEADER]
            currentWeatherCardSort.push(...this.currentWeatherCardSort)
            if (this.compareCurrentWeatherCardSort(currentWeatherCardSort) ||
            this.compareCurrentWeatherObserverCardSort(this.currentWeatherObserveCardSort)) {
              const defaultWeatherCardSort = [...Constants.DEFAULT_WEATHER_CARD_SORT]
              const currentWeatherObserveCardSort = [...Constants.DEFAULT_WEATHER_OBSERVES_CARD_SORT]
              AppRuntimeData.getInstance().setCurrentWeatherCardSort(defaultWeatherCardSort)
              AppRuntimeData.getInstance().setCurrentWeatherObservesCardSort(currentWeatherObserveCardSort)
              this.currentWeatherCardSort =
                defaultWeatherCardSort.filter(it => it != Constants.ITEM_TYPE_WEATHER_HEADER)
              this.currentWeatherObserveCardSort = currentWeatherObserveCardSort
            }
            ToastUtil.showToast('已恢复默认')
          } : undefined
        })
        Stack() {
          this.weatherCardSortList()
          this.weatherCardSortGrid()
        }
        .width('100%')
        .layoutWeight(1)
      }
      .width('100%')
      .height('100%')
    }
    .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
  leftIcon() {
    Image($r('app.media.ic_close_icon1'))
      .width(20)
      .height(20)
      .colorFilter(ColorUtils.translateColor($r('app.color.black'), this.isBackEnabled ? 1 : 0.2))
      .onClick(this.isBackEnabled ? () => {
        ZRouter.getInstance().pop()
      } : undefined)
  }

  @Builder
  weatherCardSortList() {
    List() {
      ListItem() {
        Text('首页的天气卡片将会按照以下排序进行展示')
          .height(30)
          .textAlign(TextAlign.Start)
          .align(Alignment.BottomStart)
          .padding({ left: 16 })
          .fontSize(14)
          .fontColor($r('app.color.color_999999'))
      }

      ForEach(this.currentWeatherCardSort, (item: number) => {
        WeatherCardSortListItem({
          weatherCardItemType: item,
          index: this.currentWeatherCardSort.findIndex(it => it == item),
          length: this.currentWeatherCardSort.length,
          onReorderStart: () => {
            this.isBackEnabled = false
            return true
          },
          onChangeItem: (from, to) => {
            if (ArrayUtil.isNotEmpty(this.currentWeatherCardSort)) {
              const tmp = this.currentWeatherCardSort.splice(from, 1)
              this.currentWeatherCardSort.splice(to, 0, tmp[0])
            }
          },
          onReorderDone: () => {
            this.isBackEnabled = true
            const currentWeatherCardSort = [Constants.ITEM_TYPE_WEATHER_HEADER]
            currentWeatherCardSort.push(...this.currentWeatherCardSort)
            AppRuntimeData.getInstance().setCurrentWeatherCardSort(currentWeatherCardSort)
          },
          onWeatherObserveSortTap: () => {
            const index = this.currentWeatherCardSort.findIndex(it => it == item)
            if (index >= 0) {
              this.contentOpacity = 0
              this.animDuration = 0
              this.marginTop = index * (48 + 12)
              this.tempMarginTop = this.marginTop
              setTimeout(() => {
                this.animDuration = 200
                this.marginTop = 0
                this.gridOpacity = 1
              }, 16)
            }
          }
        })
      }, (item: number) => item.toString())
    }
    .width('100%')
    .height('100%')
    .divider({ strokeWidth: 12, color: $r('app.color.transparent') })
    .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
    .opacity(this.contentOpacity)
    .animation({ curve: Curve.Ease, duration: 200 })
  }

  @Builder
  weatherCardSortGrid() {
    List() {
      ListItem() {
        Text('专业数据卡片将会按照以下排序进行展示')
          .height(30)
          .textAlign(TextAlign.Start)
          .align(Alignment.BottomStart)
          .padding({ left: 16 })
          .fontSize(14)
          .fontColor($r('app.color.color_999999'))
      }

      ListItem() {
        Row() {
          Row() {
            Text('专业数据')
              .fontSize(16)
              .fontColor($r('app.color.black'))
              .fontWeight(FontWeight.Bold)
            Image($r('app.media.ic_sort_icon'))
              .width(18)
              .height(18)
              .colorFilter(ColorUtils.translateColor($r('app.color.color_999999')))
              .margin({ left: 8 })
              .draggable(false)
          }

          Image($r('app.media.ic_close_icon1'))
            .width(18)
            .height(18)
            .colorFilter(ColorUtils.translateColor($r('app.color.color_999999')))
            .draggable(false)
            .onClick(() => {
              this.gridOpacity = 0
              this.animDuration = 200
              this.marginTop = this.tempMarginTop
              setTimeout(() => {
                this.contentOpacity = 1
              }, 200)
            })
        }
        .width('100%')
        .height(48)
        .justifyContent(FlexAlign.SpaceBetween)
        .backgroundColor($r('app.color.card_color_06'))
        .borderRadius(6)
        .padding({ left: 16, right: 16 })
      }
      .padding({ left: 16, right: 16 })
      .margin({ top: this.marginTop })
      .animation({ curve: Curve.Ease, duration: this.animDuration })

      ListItem() {
        Grid() {
          ForEach(this.currentWeatherObserveCardSort, (item: number) => {
            this.weatherCardSortGridItem(item, '100%')
          }, (item: number) => item.toString())
        }
        .width('100%')
        .scrollBar(BarState.Off)
        .columnsTemplate('1fr 1fr')
        .columnsGap(12)
        .rowsGap(12)
        .maxCount(2)
        .padding({ left: 16, right: 16 })
        .layoutDirection(GridDirection.Row)
        .opacity(this.gridOpacity)
        .animation({ curve: Curve.Ease, duration: 200 })
        .editMode(true)
        .supportAnimation(true)
        .onItemDragStart((_, itemIndex: number) => {
          this.isBackEnabled = false
          return this.pixelMapBuilder(this.currentWeatherObserveCardSort[itemIndex])
        })
        .onItemDrop((_, itemIndex: number, insertIndex: number, isSuccess: boolean) => {
          if (!isSuccess || insertIndex >= this.currentWeatherObserveCardSort.length) {
            return
          }
          this.isBackEnabled = true
          this.changeGridIndex(itemIndex, insertIndex)
        })
      }
    }
    .width('100%')
    .height('100%')
    .divider({ strokeWidth: 12, color: $r('app.color.transparent') })
    .edgeEffect(EdgeEffect.Spring, { alwaysEnabled: true })
    .backgroundColor($r('app.color.bg_color'))
    .opacity(1 - this.contentOpacity)
    .animation({ curve: Curve.Ease, duration: 200 })
    .visibility(this.contentOpacity == 1 ? Visibility.Hidden : Visibility.Visible)
  }

  @Builder
  weatherCardSortGridItem(itemType: number, width: number | string) {
    GridItem() {
      Row() {
        Text(this.getTitle(itemType))
          .fontSize(16)
          .fontColor($r('app.color.black'))
          .fontWeight(FontWeight.Bold)

        Image($r('app.media.ic_menu_icon'))
          .width(24)
          .height(24)
          .colorFilter(ColorUtils.translateColor($r('app.color.color_999999')))
          .draggable(false)
      }
      .width(width)
      .height(48)
      .justifyContent(FlexAlign.SpaceBetween)
      .backgroundColor($r('app.color.card_color_06'))
      .borderRadius(6)
      .padding({ left: 16, right: 16 })
    }
  }

  @Builder
  pixelMapBuilder(itemType: number) {
    this.weatherCardSortGridItem(itemType, (px2vp(DisplayUtil.getWidth()) - 2 * 16 - 12) / 2)
  }

  changeGridIndex(index1: number, index2: number) {
    let tmp = this.currentWeatherObserveCardSort.splice(index1, 1)
    this.currentWeatherObserveCardSort.splice(index2, 0, tmp[0])
    AppRuntimeData.getInstance().setCurrentWeatherObservesCardSort(this.currentWeatherObserveCardSort)
  }

  getTitle(itemType: number): string {
    switch (itemType) {
      case Constants.ITEM_TYPE_OBSERVE_UV:
        return "紫外线指数"
      case Constants.ITEM_TYPE_OBSERVE_SHI_DU:
        return "湿度"
      case Constants.ITEM_TYPE_OBSERVE_TI_GAN:
        return "体感温度"
      case Constants.ITEM_TYPE_OBSERVE_WD:
        return "风向";
      case Constants.ITEM_TYPE_OBSERVE_SUNRISE_SUNSET:
        return "日出日落"
      case Constants.ITEM_TYPE_OBSERVE_PRESSURE:
        return "气压"
      case Constants.ITEM_TYPE_OBSERVE_VISIBILITY:
        return "可见度"
      case Constants.ITEM_TYPE_OBSERVE_FORECAST40:
        return "未来40日天气"
    }
    return ""
  }

  compareCurrentWeatherCardSort(currentWeatherCardSort: Array<number>): boolean {
    if (currentWeatherCardSort.length != Constants.DEFAULT_WEATHER_CARD_SORT.length) {
      return false
    }
    for (let index = 0; index < currentWeatherCardSort.length; index++) {
      if (Constants.DEFAULT_WEATHER_CARD_SORT[index] != currentWeatherCardSort[index]) {
        return true
      }
    }
    return false
  }

  compareCurrentWeatherObserverCardSort(currentWeatherObserverCardSort: Array<number>): boolean {
    if (currentWeatherObserverCardSort.length !=
    Constants.DEFAULT_WEATHER_OBSERVES_CARD_SORT.length) {
      return false
    }
    for (let index = 0; index < currentWeatherObserverCardSort.length; index++) {
      if (Constants.DEFAULT_WEATHER_OBSERVES_CARD_SORT[index] != currentWeatherObserverCardSort[index]) {
        return true
      }
    }
    return false
  }
}

WeatherCardSortListItem

@ComponentV2
export struct WeatherCardSortListItem {
  @Param @Require weatherCardItemType: number
  @Param index: number = 0
  @Param length: number = 0
  @Event onReorderStart?: () => boolean = undefined
  @Event beforeChange?: (target: number) => boolean = undefined
  @Event onChangeItem?: (from: number, to: number) => void = undefined
  @Event onReorderDone?: () => void = undefined
  @Event onWeatherObserveSortTap?: () => void = undefined
  private isLongPress: boolean = false
  private isMenuPress: boolean = false
  private dragRefOffset: number = 0
  @Local zIndexValue: number = 0
  @Local offsetY: number = 0

  build() {
    ListItem() {
      Row() {
        Row() {
          Text(this.title)
            .fontSize(16)
            .fontColor($r('app.color.black'))
            .fontWeight(FontWeight.Bold)
          Image($r('app.media.ic_sort_icon'))
            .width(18)
            .height(18)
            .colorFilter(ColorUtils.translateColor($r('app.color.color_999999')))
            .margin({ left: 8 })
            .draggable(false)
            .visibility(this.weatherCardItemType == Constants.ITEM_TYPE_OBSERVE ? Visibility.Visible :
            Visibility.Hidden)
        }

        Image($r('app.media.ic_menu_icon'))
          .width(24)
          .height(24)
          .colorFilter(ColorUtils.translateColor($r('app.color.color_999999')))
          .draggable(false)
          .gesture(
            GestureGroup(GestureMode.Sequence,
              PanGesture()
                .onActionStart(() => {
                  this.onMenuPress()
                })
                .onActionUpdate((event: GestureEvent) => {
                  this.onItemMove(event.offsetY)
                })
                .onActionEnd(() => {
                  this.onItemDrop()
                })
            ).onCancel(() => {
              if (!this.isMenuPress) {
                return;
              }
              this.onItemDrop()
            })
          )
      }
      .width('100%')
      .height(ITEM_HEIGHT)
      .justifyContent(FlexAlign.SpaceBetween)
      .backgroundColor($r('app.color.card_color_06'))
      .borderRadius(6)
      .padding({ left: 16, right: 16 })
      .gesture(
        GestureGroup(GestureMode.Sequence,
          LongPressGesture()
            .onAction(() => {
              this.onLongPress()
            }),
          PanGesture()
            .onActionUpdate((event: GestureEvent) => {
              this.onItemMove(event.offsetY)
            })
            .onActionEnd(() => {
              this.onItemDrop()
            })
        ).onCancel(() => {
          if (!this.isLongPress) {
            return
          }
          this.onItemDrop()
        })
      )
      .onClick(this.weatherCardItemType == Constants.ITEM_TYPE_OBSERVE ? this.onWeatherObserveSortTap : undefined)
    }
    .padding({ left: 16, right: 16 })
    .zIndex(this.zIndexValue)
    .translate({ y: this.offsetY })
  }

  get title() {
    switch (this.weatherCardItemType) {
      case Constants.ITEM_TYPE_ALARMS:
        return "极端天气"
      case Constants.ITEM_TYPE_AIR_QUALITY:
        return "空气质量"
      case Constants.ITEM_TYPE_HOUR_WEATHER:
        return "每小时天气预报"
      case Constants.ITEM_TYPE_DAILY_WEATHER:
        return "15日天气预报"
      case Constants.ITEM_TYPE_OBSERVE:
        return "专业数据"
      case Constants.ITEM_TYPE_LIFE_INDEX:
        return "生活指数"
    }
    return ""
  }

  onLongPress() {
    const enable = this.onReorderStart ? this.onReorderStart() : true
    if (enable) {
      this.zIndexValue = 1
      this.isLongPress = true
      this.dragRefOffset = 0
    }
  }

  onMenuPress() {
    const enable = this.onReorderStart ? this.onReorderStart() : true
    if (enable) {
      this.zIndexValue = 1
      this.isMenuPress = true
      this.dragRefOffset = 0
    }
  }

  onItemMove(offsetY: number) {
    if (!this.isLongPress && !this.isMenuPress) {
      return
    }
    this.offsetY = offsetY - this.dragRefOffset
    const direction = this.offsetY > 0 ? 1 : -1
    if (Math.abs(this.offsetY) > ITEM_HEIGHT / 2) {
      if (this.index === 0 && direction === -1) {
        return
      }
      if (this.index === this.length - 1 && direction === 1) {
        return
      }
      // 目标位置索引
      const target = this.index + direction
      if (this.beforeChange) {
        if (this.beforeChange(target)) {
          return
        }
      }
      animateTo({ curve: Curve.Friction, duration: 300 }, () => {
        this.offsetY -= direction * ITEM_HEIGHT
        this.dragRefOffset += direction * ITEM_HEIGHT
        if (target !== -1 && target <= this.length) {
          console.log('changeItem index = ' + this.index + ' target = ' + target)
          if (this.onChangeItem) {
            this.onChangeItem(this.index, target)
          }
        }
      })
    }
  }

  onItemDrop() {
    if (this.isLongPress || this.isMenuPress) {
      this.isLongPress = false
      this.isMenuPress = false
      this.dragRefOffset = 0;
      if (this.onReorderDone) {
        this.onReorderDone()
      }
      animateTo({
        curve: curves.interpolatingSpring(14, 1, 170, 17), onFinish: () => {
          this.zIndexValue = 0
        }
      }, () => {
        this.offsetY = 0
      })
    }
  }
}

天气卡片排序之后

onReorderDone: () => {
  this.isBackEnabled = true
  const currentWeatherCardSort = [Constants.ITEM_TYPE_WEATHER_HEADER]
  currentWeatherCardSort.push(...this.currentWeatherCardSort)
  AppRuntimeData.getInstance().setCurrentWeatherCardSort(currentWeatherCardSort)
},
setCurrentWeatherCardSort(currentWeatherCardSort: Array<number>) {
  this._currentWeatherCardSort = currentWeatherCardSort
  PreferencesUtil.put(Constants.CURRENT_WEATHER_CARD_SORT, currentWeatherCardSort)
    .then(() => {
      EmitterUtil.post(EmitterManager.WEATHER_CARD_SORT_CHANGED_EVENT, undefined)
    })
}
EmitterUtil.onSubscribe<undefined>(EmitterManager.WEATHER_CARD_SORT_CHANGED_EVENT, () => {
  this.weatherMainVM.reorder(AppRuntimeData.getInstance().currentWeatherCardSort)
})
reorder(currentWeatherSort: Array<number>) {
  const newWeatherItems: Array<WeatherItemData> = []
  currentWeatherSort.forEach(itemType => {
    const find = this.weatherItems?.find(it => it.itemType == itemType)
    if (!ObjectUtil.isNull(find)) {
      newWeatherItems.push(find!)
    }
  })
  this.weatherItems = newWeatherItems
}

天气专业数据卡片排序后

changeGridIndex(index1: number, index2: number) {
  let tmp = this.currentWeatherObserveCardSort.splice(index1, 1)
  this.currentWeatherObserveCardSort.splice(index2, 0, tmp[0])
  AppRuntimeData.getInstance().setCurrentWeatherObservesCardSort(this.currentWeatherObserveCardSort)
}
setCurrentWeatherObservesCardSort(currentWeatherObservesCardSort: Array<number>) {
  this._currentWeatherObservesCardSort = currentWeatherObservesCardSort
  PreferencesUtil.put(Constants.CURRENT_WEATHER_OBSERVES_CARD_SORT, currentWeatherObservesCardSort)
    .then(() => {
      EmitterUtil.post(EmitterManager.WEATHER_OBSERVES_CARD_SORT_CHANGED_EVENT, undefined)
    })
}
EmitterUtil.onSubscribe<undefined>(EmitterManager.WEATHER_OBSERVES_CARD_SORT_CHANGED_EVENT, () => {
  this.weatherMainVM.reorderObserves()
})
reorderObserves() {
  this.generateWeatherItems(this.weatherItems?.[0].weatherData)
}
private generateWeatherItems(weatherData?: WeatherData) {
  // 生成天气背景
  this.weatherBg = WeatherDataUtils.generateWeatherBg(weatherData)
  // 根据天气背景计算天气头部是否是dark模式
  this.isWeatherHeaderDark = WeatherDataUtils.isWeatherHeaderDark(this.weatherBg)
  // 根据天气背景计算天气内容是否是dark模式
  this.isDark = WeatherDataUtils.isDark(this.weatherBg)
  // 根据天气背景计算天气面板的透明度
  this.panelOpacity = WeatherDataUtils.calPanelOpacity(this.weatherBg)
  this.itemTypeObserves =
    WeatherDataUtils.getItemTypeObserves(AppRuntimeData.getInstance().currentWeatherObservesCardSort,
      Constants.ITEM_TYPE_OBSERVE, weatherData)
  // 生成天气items数据
  this.weatherItems = WeatherDataUtils.generateWeatherItems(AppRuntimeData.getInstance().currentWeatherCardSort,
    this.itemTypeObserves, weatherData)
}