易得天气
完成了天气预报主界面到城市列表的一镜到底效果
首先上效果图(天气预报主界面还未完成)
效果图
一镜到底效果主要分成两部分:
1、天气预报主界面缩放到城市列表对应item位置
2、城市列表在List显示区域做一个从上至下/从下至上的item动画
效果一
1、使用clipShape函数对天气预报主界面进行裁剪
2、使用Animator做动画让天气预报主界面的裁剪动起来
clipShape
arduino
.clipShape(new PathShape({ commands: this.weatherMainVM.pathCommands }))
kotlin
this.weatherMainVM.pathCommands = this.buildPathCommands(0)
this.weatherMainVM.animatorOptions = this.getUIContext().createAnimator({
duration: 400,
delay: 0,
easing: 'ease',
iterations: 1,
fill: "forwards",
direction: 'normal',
begin: 0,
end: 1
})
this.weatherMainVM.animatorOptions.onFrame = (animValue: number) => {
this.weatherMainVM.animValue = animValue
this.weatherMainVM.pathCommands = this.buildPathCommands(animValue)
this.weatherMainVM.contentVisibility = animValue >= 1 ? Visibility.Hidden : Visibility.Visible
}
this.weatherMainVM.animatorOptions.onFinish = () => {
if (this.weatherMainVM.isShowCityManagerPage) {
if (this.showCityList) {
this.showCityList()
}
} else {
if (this.hideCityList) {
this.hideCityList()
}
}
}
typescript
private buildPathCommands(animValue: number): string {
const radius = vp2px(16)
const offsetY = this.weatherMainVM.offsetY
const itemHeight = vp2px(98)
const vy =
(DisplayUtil.getHeight() - offsetY - itemHeight) * (1 - animValue) + offsetY + itemHeight - radius * animValue
return `M ${radius * animValue} ${(offsetY + radius) *
animValue} Q ${radius * animValue} ${offsetY *
animValue}, ${2 * radius * animValue} ${offsetY *
animValue} H ${DisplayUtil.getWidth() - 2 * radius * animValue} Q ${DisplayUtil.getWidth() -
radius * animValue} ${offsetY * animValue},
${DisplayUtil.getWidth() - radius * animValue} ${(offsetY + radius) *
animValue} V ${vy} Q ${DisplayUtil.getWidth() - radius * animValue}
${vy + radius * animValue}, ${DisplayUtil.getWidth() -
2 * radius * animValue} ${vy + radius * animValue}
H ${2 * radius * animValue} Q ${radius * animValue}
${vy + radius * animValue}, ${radius * animValue} ${vy} Z`
}
path commands命令主要用到了以下几个命令:
M = moveto 起始
H = horizontal lineto 水平线
V = vertical lineto 垂直线
Q = quadratic Bézier curve 二次贝塞尔曲线
Z = closepath 闭合(从最后一个点连直线到起始点)
使用大写字母表示绝对位置,小写字母表示相对位置(相对于起点的位置,向右向下为正)。
其中Q命令起点、控制点、终点在一个点就是直角
获取城市列表item在屏幕当中的位置信息
主要通过componentUtils.getRectangleById(id)函数获取item的位置信息
城市列表设置id
bash
.id('city_list')
城市列表item设置id
javascript
.id(`city_item_${this.index}`)
获取城市列表item位置信息,如果该item不在List显示区域内,会将该item移动到List显示区域内
ini
checkItemBorder(addedCityData?: Array<CityData>): componentUtils.ComponentInfo | undefined {
const currentCityData = AppRuntimeData.getInstance().currentCityData
if (!ObjectUtil.isNull(currentCityData)) {
const findIndex =
addedCityData?.findIndex(it => it.cityid == currentCityData!.cityid) ?? -1
if (findIndex >= 0) {
const itemRectangle = componentUtils.getRectangleById(`city_item_${findIndex}`)
const cityListRectangle = componentUtils.getRectangleById('city_list')
if (itemRectangle.localOffset.y < 0) {
this.listScroller.scrollToIndex(findIndex + 1)
itemRectangle.windowOffset.y = cityListRectangle.windowOffset.y
} else if (itemRectangle.localOffset.y + itemRectangle.size.height > cityListRectangle.size.height) {
const dy = itemRectangle.size.height - (cityListRectangle.size.height - itemRectangle.localOffset.y)
this.listScroller.scrollBy(0, px2vp(dy))
itemRectangle.windowOffset.y = itemRectangle.windowOffset.y - dy
}
return itemRectangle
}
}
return undefined
}
还有一点比较重要,需要给List设置cachedCount
官方文档的一句话
懒加载、非懒加载都只布局List显示区域+List显示区域外cachedCount的内容。
如果不设置cachedCount,在List显示区域外的item将拿不到位置信息
kotlin
.cachedCount(this.addedCityData?.length)
效果二
首先给城市列表item添加三个参数
less
@Param startIndex: number = 0
@Param endIndex: number = 0
@Param itemOpacity: number = 0
startIndex < endIndex表示List item从startIndex开始到endIndex结束做item动画
startIndex > endindex表示List item从endIndex开始到startIndex结束做item动画
itemOpacity表示item的显示透明度
item加上以下函数
kotlin
.opacity(this.itemOpacity)
.scale({ x: this.itemOpacity <= 0 ? 0.9 : 1, y: this.itemOpacity <= 0 ? 0.9 : 1 })
.animation({ duration: this.calDuration(), curve: Curve.Linear })
kotlin
calDuration(): number {
if (this.startIndex <= 0 && this.endIndex <= 0) {
return 0
}
if (this.startIndex > this.endIndex) {
if (this.index < this.endIndex || this.index > this.startIndex) {
return 0
}
const fixedIndex = this.startIndex - this.index
return fixedIndex * 100 + 50
} else {
if (this.index < this.startIndex || this.index > this.endIndex) {
return 0
}
const fixedIndex = this.index - this.startIndex
return fixedIndex * 100 + 50
}
}
下面做个演示
加个点击事件
kotlin
this.cityManagerVm.itemOpacity = this.cityManagerVm.itemOpacity == 0 ? 1 : 0
php
CityManagerItem({
。。。
startIndex: 0,
endIndex: 5,
itemOpacity: this.cityManagerVm.itemOpacity,
。。。
})
startIndex=0,endIndex=5的效果图
startIndex=3,endIndex=5的效果图
startIndex=5,endIndex=3的效果图
所以要确定startIndex和endIndex的值就可以做城市列表item的动画
ini
showCityList(addedCityData?: Array<CityData>) {
const currentCityData = AppRuntimeData.getInstance().currentCityData
if (!ObjectUtil.isNull(currentCityData)) {
const findIndex =
addedCityData?.findIndex(it => it.cityid == currentCityData!.cityid) ?? -1
if (findIndex >= 0) {
const findRectangle = componentUtils.getRectangleById(`city_item_${findIndex}`)
const cityListRectangle = componentUtils.getRectangleById('city_list')
const indexes =
addedCityData?.map((_, index) => index).filter((_, index) => {
const itemRectangle = componentUtils.getRectangleById(`city_item_${index}`)
return itemRectangle.localOffset.y + itemRectangle.size.height > 0 &&
itemRectangle.localOffset.y < cityListRectangle.size.height
})
if (indexes) {
if (findRectangle.localOffset.y > cityListRectangle.size.height * 0.5) {
this.startIndex = indexes[indexes.length - 1]
this.endIndex = indexes[0]
} else {
this.startIndex = indexes[0]
this.endIndex = indexes[indexes.length - 1]
}
this.itemOpacity = 1
}
}
}
}
javascript
hideCityList() {
this.itemOpacity = 0
}
好了,接下来就是做天气预报主界面的UI了