易得天气
天气预报主界面,目前完成了头部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
}
}