一、背景
继上篇之后,继续对项目进行优化,本篇也是关于常见的交互实现,即Swiper左右滑动的伸缩效果
二、目标
如上图所示,在滑动过程中,移走的缩小,移入的放大,同时展示相邻两项的一部分,这在app中也是一种很常见的交互。
三、实现步骤
在开始之前,我们可以先回顾一下原来Android中的实现;步骤也很简单,我们在ViewPage2外层设置可以突破边界,即:android:clipChildren="false", 然后PageTransformer来实现,滑动过程中的的动画;代码如下:
mBinding.vp2SquareChild.setPageTransformer(PetPageTransformer())
使用setPageTransformer给ViewPage2设置PageTransformer对象
class PetPageTransformer : ViewPager2.PageTransformer {
override fun transformPage(page: View, position: Float) {
page.setPadding(
page.context.resources.getDimension(R.dimen.d_6).toInt(),
0,
page.context.resources.getDimension(R.dimen.d_6).toInt(),
0
)
var scale = 0.0f
var alpha = 0.0f
if(position in 0.0f..1.0F) { //1
scale = 1.0f - (position * 0.1f)
alpha = 1.0f - (position * 0.5f)
}else if (-1.0f <= position && position < 0.0f){ //2
scale = (position * 0.1f) + 1.0f
alpha = (position * 0.5f) + 1.0f
}
page.scaleY = scale
page.alpha = alpha
}
}
PageTransformer内部做两件事:
- 设置padding
加上前面的android:clipChildren="false" ,实现相邻项的部分展示
- 根据返回的position做动画处理
position为正数或负数表示相邻项的移动比例,根据这个比例换算出动画执行的属性值,且设置给view即可。
关于Android的实现就完成了,那鸿蒙上实现会不会类似呢,我觉得差不多,哈哈:
1、设置相邻项边距
我们需要相邻项展示部分内容的效果,如下图:
在鸿蒙中这个比较简单,只要给Swiper设置下面的属性即可:
.prevMargin(this.swiperMargin) // 漏出前一项的部分
.nextMargin(this.swiperMargin)// 漏出后一项的部分
2、设置相邻项的缩放动画
我们知道Android中是通过transformPage(page: View, position: Float) 函数回调来处理的,Swiper也有类似的函数,这就是:
.onGestureSwipe((index, event)
index我们可以理解,指当前展示的项,event是SwiperAnimationEvent:
declare interface SwiperAnimationEvent {
/**
* Offset of the current page to the start position of the swiper main axis. The unit is vp.
*
* @type { number }
* @default 0.0 vp
* @syscap SystemCapability.ArkUI.ArkUI.Full
* @since 10
*/
/**
* Offset of the current page to the start position of the swiper main axis. The unit is vp.
*
* @type { number }
* @default 0.0 vp
* @syscap SystemCapability.ArkUI.ArkUI.Full
* @crossplatform
* @atomicservice
* @since 11
*/
currentOffset: number;
/**
* Offset of the target page to the start position of the swiper main axis. The unit is vp.
*
* @type { number }
* @default 0.0 vp
* @syscap SystemCapability.ArkUI.ArkUI.Full
* @since 10
*/
/**
* Offset of the target page to the start position of the swiper main axis. The unit is vp.
*
* @type { number }
* @default 0.0 vp
* @syscap SystemCapability.ArkUI.ArkUI.Full
* @crossplatform
* @atomicservice
* @since 11
*/
targetOffset: number;
/**
* Start speed of the page-turning animation. The unit is vp/s.
*
* @type { number }
* @default 0.0 vp/s
* @syscap SystemCapability.ArkUI.ArkUI.Full
* @since 10
*/
/**
* Start speed of the page-turning animation. The unit is vp/s.
*
* @type { number }
* @default 0.0 vp/s
* @syscap SystemCapability.ArkUI.ArkUI.Full
* @crossplatform
* @atomicservice
* @since 11
*/
velocity: number;
}
有三个参数:
- currentOffset
Swiper当前显示元素在主轴方向上,相对于Swiper起始位置的位移。单位VP,默认值为0。
- targetOffset
Swiper动画目标元素在主轴方向上,相对于Swiper起始位置的位移。单位VP,默认值为0。
- velocity
Swiper离手动画开始时的离手速度。单位VP/S,默认值为0。
但是我想说的是,在使用过程中,我发现只有currentOffset是有值的,其他的都是0,(有没有知道这是为啥)。
既然只有currentOffset有值,那就那这个判断吧。同时还有个情况,因为是命令式编程,Android在transformPage(page: View, position: Float) 的回调中可以直接获取page这个View对象修改,但是鸿蒙是响应式的,所以我们不能直接改,应该是要设置个变量,然后,将变量设置给Component,同时因为是多个Component,所以要建一个Array来存储每个Component的scale数据。
.onGestureSwipe((index, event) => {
const currentOffset = event.currentOffset;
const targetOffset = event.targetOffset;
const velocity = event.velocity;
console.debug(`currentOffset -> ${currentOffset} `)
console.debug(`targetOffset -> ${targetOffset} `)
console.debug(` index -> ${index} `)
console.debug(` velocity -> ${velocity} `)
if (currentOffset < 0) {
if (this.isIndexValid(this.currentSwiperIndex + 1)) {
const changeScale = (this.minScale + (px2vp(-currentOffset) * 2 / this.displayWidth * 0.4))
this.cardsScale[this.currentSwiperIndex + 1] = changeScale > this.maxScale ? this.maxScale : changeScale
}
if (this.isIndexValid(this.currentSwiperIndex)) {
const start = this.cardsScale[this.currentSwiperIndex]
const changeScale = this.maxScale - (px2vp(-currentOffset) * 2 / this.displayWidth * 0.4)
this.cardsScale[this.currentSwiperIndex] = changeScale < this.minScale ? this.minScale : changeScale
}
if (this.isIndexValid(this.currentSwiperIndex - 1)) {
this.cardsScale[this.currentSwiperIndex - 1] = this.minScale;
}
} else {
if (this.isIndexValid(this.currentSwiperIndex - 1)) {
const changeScale = (this.minScale + (px2vp(currentOffset) * 2 / this.displayWidth * 0.4))
this.cardsScale[this.currentSwiperIndex - 1] = changeScale > this.maxScale ? this.maxScale : changeScale
}
if (this.isIndexValid(this.currentSwiperIndex)) {
const start = this.cardsScale[index]
const changeScale = this.maxScale - (px2vp(currentOffset) * 2 / this.displayWidth * 0.4)
this.cardsScale[this.currentSwiperIndex] = changeScale < this.minScale ? this.minScale : changeScale
}
if (this.isIndexValid(index + 1)) {
this.cardsScale[index + 1] = this.minScale
}
}
})
根据currentOffset的正负值可以判断,当前的滑动反向,通过方向,我们根据index为基准,分别设置前后项的缩放属性值,因为currentOffset是具体的滑动偏移量,不是比例,所以要根据偏移量与屏幕的比例(这个值可以自己调整) 来转换成缩放值。
有时可能上面的转换值会有误差,所以在onChange的回调里面做一个校准:
private maxScale = 1.0
private minScale = 0.9
.onChange((index) => {
if (this.isIndexValid(index + 1)) {
this.cardsScale[index + 1] = this.minScale
}
if (this.isIndexValid(index)) {
this.cardsScale[index] = this.maxScale
}
if (this.isIndexValid(index - 1)) {
this.cardsScale[index - 1] = this.minScale;
}
})
3、其他问题
上面的流程结束后,基本功能就完成了,还有点遗留问题,就是我们在滑动过程中,如果此时没有滑动出一页,手指离开屏幕,Swiper会滚动回去:
我们发现中间变小的卡片在回弹之后,并没有回到之前的缩放比例,是因为在我们的手指弹起后,onGestureSwipe回调不会继续执行了,所以后续的事件没有了;
由于此时没有onGestureSwipe这个事件了,我们不能直接解决这个问题,只能先从侧面做一些修补。根据场景可知,我们需要在手指弹起的时候,做一个校准,相当于前面的onChange回调的内容,于是我们就不用前面的onChange,直接在onTouch回调之后,判断手势为TouchType.Up时,做个延迟调用一下上面onChange的内容即可。
.onTouch((event)=>{
// 如果手指起来的时候,并没有滑动出一页,则要置回原有的缩放状态
if(event.type === TouchType.Up){
setTimeout(()=>{
if (this.isIndexValid(this.currentSwiperIndex + 1)) {
this.cardsScale[this.currentSwiperIndex + 1] = this.minScale
}
if (this.isIndexValid(this.currentSwiperIndex)) {
this.cardsScale[this.currentSwiperIndex] = this.maxScale
}
if (this.isIndexValid(this.currentSwiperIndex - 1)) {
this.cardsScale[this.currentSwiperIndex - 1] = this.minScale;
}
},200)
}
})
这里使用setTimeout是因为我们使用 .index($$this.currentSwiperIndex),且事件触发在onTouch中,currentSwiperIndex可能还没有生效,没有生效改的就是旧的,没有意义,还会错乱。
4、完整代码
@Component
export struct CardSwiperView {
// 卡片数据源
@Link @Watch('onDataUpdated') data: Array<PetBodyType>
@BuilderParam itemBody: (item: PetBodyType, index: number) => void;
// 卡片数据列表
@State private cardsList: SwiperIteInfo[] = [];
// 卡缩放度列表
@State private cardsScale: number[] = [];
// 屏幕宽度
private displayWidth: number = 0;
// Swiper 两侧的偏移量
private swiperMargin: number = 40;
// Swiper 当前索引值
@State private currentSwiperIndex: number = 0;
private maxScale = 1.0
private minScale = 0.9
onDataUpdated(options: Array<PetBodyType>): void {
this.updateCache()
}
aboutToAppear(): void {
const displayData: display.Display = display.getDefaultDisplaySync();
this.displayWidth = px2vp(displayData.width);
this.updateCache()
}
private updateCache() {
this.cardsList = []
this.cardsScale = []
this.data.forEach((item, index) => {
this.cardsList.push(item);
if (index === this.currentSwiperIndex) {
this.cardsScale.push(this.maxScale);
} else {
this.cardsScale.push(this.minScale);
}
})
}
build() {
Column() {
Swiper() {
ForEach(this.data, (item: PetBodyType, index: number) => {
if (this.itemBody) {
Row() {
this.itemBody(item, index)
}.scale({ x: this.cardsScale[index], y: this.cardsScale[index] })
.animation({
duration:100
})
}
}, (item: PetBodyType, index: number) => JSON.stringify(item))
}
.margin({ top: 20 })
.loop(true)
.indicator(false)
.index($$this.currentSwiperIndex)
.prevMargin(this.swiperMargin)
.nextMargin(this.swiperMargin)
.duration(200)
.curve(Curve.Friction)
.onTouch((event)=>{
// 如果手指起来的时候,并没有滑动出一页,则要置回原有的缩放状态
if(event.type === TouchType.Up){
setTimeout(()=>{
if (this.isIndexValid(this.currentSwiperIndex + 1)) {
this.cardsScale[this.currentSwiperIndex + 1] = this.minScale
}
if (this.isIndexValid(this.currentSwiperIndex)) {
this.cardsScale[this.currentSwiperIndex] = this.maxScale
}
if (this.isIndexValid(this.currentSwiperIndex - 1)) {
this.cardsScale[this.currentSwiperIndex - 1] = this.minScale;
}
},200)
}
})
.onGestureSwipe((index, event) => {
const currentOffset = event.currentOffset;
const targetOffset = event.targetOffset;
const velocity = event.velocity;
console.debug(`currentOffset -> ${currentOffset} `)
console.debug(`targetOffset -> ${targetOffset} `)
console.debug(` index -> ${index} `)
console.debug(` velocity -> ${velocity} `)
if (currentOffset < 0) {
if (this.isIndexValid(this.currentSwiperIndex + 1)) {
const changeScale = (this.minScale + (px2vp(-currentOffset) * 2 / this.displayWidth * 0.4))
this.cardsScale[this.currentSwiperIndex + 1] = changeScale > this.maxScale ? this.maxScale : changeScale
}
if (this.isIndexValid(this.currentSwiperIndex)) {
const start = this.cardsScale[this.currentSwiperIndex]
const changeScale = this.maxScale - (px2vp(-currentOffset) * 2 / this.displayWidth * 0.4)
this.cardsScale[this.currentSwiperIndex] = changeScale < this.minScale ? this.minScale : changeScale
}
if (this.isIndexValid(this.currentSwiperIndex - 1)) {
this.cardsScale[this.currentSwiperIndex - 1] = this.minScale;
}
} else {
if (this.isIndexValid(this.currentSwiperIndex - 1)) {
const changeScale = (this.minScale + (px2vp(currentOffset) * 2 / this.displayWidth * 0.4))
this.cardsScale[this.currentSwiperIndex - 1] = changeScale > this.maxScale ? this.maxScale : changeScale
}
if (this.isIndexValid(this.currentSwiperIndex)) {
const start = this.cardsScale[index]
const changeScale = this.maxScale - (px2vp(currentOffset) * 2 / this.displayWidth * 0.4)
this.cardsScale[this.currentSwiperIndex] = changeScale < this.minScale ? this.minScale : changeScale
}
if (this.isIndexValid(index + 1)) {
this.cardsScale[index + 1] = this.minScale
}
}
})
.onAnimationStart((index, targetIndex) => {
})
.height("100%")
.width("100%")
}
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center)
}
isIndexValid(index: number): boolean {
return index >= 0 && index < this.cardsList.length;
}
}
export interface SwiperIteInfo {
scale:number
}
export class PetBodyType implements SwiperIteInfo{
scale: number = 1
shapeType: string = ''
shapeTitle: string = ''
shapeDesc: string = ''
shapeUrl: string = ''
}
四、总结
这篇只是简单说下Swiper华东的动效实现,如果复杂一点的也可以通过onGestureSwipe来实现,只是比Android麻烦的是需要缓存这些动画属性数据。最后就没有最后了。