这篇文章记录实现图片画廊形式展示,中间大两侧渐小,可以垂直或水平滑动切换,无限循环展示。
实现效果

演示.gif
实现思路
1.想要实现卡牌堆叠的效果,因此需要用到stack布局,让最上面的卡牌zIndex最大,两侧的zIndex依次减小,不需要显示的zIndex设置为0被中间的卡牌挡住,或者隐藏也可以。
2.想要实现中间大两侧小的效果,需要通过中间卡牌的index,拿到上面/左侧显示的index,下面/右侧显示的index,通过间隔控制两侧卡牌的大小和相对中间卡牌的位移。
3.通过添加滑动手势,判断滑动方向,使居中卡牌的index加/减1
计算难点
有了实现思路,实现整个效果的核心计算点是需要计算所有卡牌相对于当前居中卡牌的位置。这里分两种情况。假如卡牌总数是为6:
1.卡牌按照顺序依次显示

顺序展示.png
如图所示,当展示图片索引是顺序展示时,上面的卡牌与中间的卡牌间隔依次为1、2、3下面的卡牌与中间的卡牌间隔依次为-1、-2、-3
因此,可以根据index间隔的差值,设置其他卡牌的大小和位置。
注意这里的差值最大值为 数组的size/2向下取整
2.卡牌展示为边界情况时

边界展示.png
当遇到边界情况时,如同所示,下面的卡牌与居中卡牌的index插值为5、4,大于数组的size/2。这时需要通过与size的差值做判断,图中所示下面的卡牌差值与size的差为-1、-2,因此是在居中卡牌的下面1、2个间隔单位。
源码
@Entry
@ComponentV2
struct GalleryEffectTest{
pathStack : NavPathStack = new NavPathStack()
@Local galleryList:Array<ResourceStr> = [$r('app.media.img_gallery_1'),$r('app.media.img_gallery_2'),$r('app.media.img_gallery_3'),$r('app.media.img_gallery_4'),$r('app.media.img_gallery_5'),$r('app.media.img_gallery_6')]
@Local showCount:number = 5 // 显示图片数量
@Local topImgIndex:number = Math.floor(this.showCount/2) //顶部图片索引
@Local imgOffSetY:number=100 // 相邻图片Y偏移量
@Local offsetCoefficients:number=10 // 减少偏移系数 间隔越大的图片 偏移越小
@Local marginBottom: number = 0;
@Local changedIndex: boolean = true;
@Local directionvertical:boolean = true
/**
* 默认情况 当展示5个 图片索引为 0 1 2 3 4 2为中间顶部
* let coefficient = this.topImgIndex - index; 返回 2,1 0 -1,-2
* 0 不偏移 1 -1 上下便偏移1个单位 2 -2 上下偏移2个单位
* 特殊情况 图片索引为 4 5 0 1 2 0为中间顶部
* let coefficient = this.topImgIndex - index; -4 -5 返回 0 -1 -2
* 边界问题特殊处理 -4 -5
* tempOffset 2 1 返回 2 1
* 总结:当默认展示5个卡片时 这里始终返回 2,1 0 -1,-2 可以根据返回值设置图片大小和位置
*/
getImgCoefficients(index: number): number {
let coefficient = this.topImgIndex - index; // 3 2 1 0 -1 -2
let tempCoefficient = Math.abs(coefficient);
if (tempCoefficient <= Math.floor(this.showCount/2)) {
return coefficient;
}
//处理边界情况
let tempOffset = this.galleryList.length - tempCoefficient;
if (tempOffset <= Math.floor(this.showCount/2)) {
if (coefficient > 0) {
return -tempOffset;
}
return tempOffset;
}
return 0;
}
/**
*计算相对居中卡牌的偏移
* @param index
* @returns
*/
getOffSetY(index: number): number {
let offsetIndex = this.getImgCoefficients(index);
let tempOffset = Math.abs(offsetIndex);
let offsetY = this.marginBottom / (tempOffset + 1);;
if (tempOffset === 1) {
offsetY += -offsetIndex * this.imgOffSetY;
} else {
offsetY += -offsetIndex * (this.imgOffSetY - this.offsetCoefficients);
}
return offsetY;
}
handlePanGesture(offsetY: number): void {
if (Math.abs(offsetY) < 50) {
this.marginBottom = offsetY;
} else {
if (this.changedIndex) {
return;
}
this.changedIndex = true;
this.startAnimation(offsetY < 0);
}
}
//滑动动画,修改居中卡牌的index,重新布局
startAnimation(isUp: boolean): void {
animateTo({
duration: 300,
}, () => {
let dataLength = this.galleryList.length;
let tempIndex = isUp ? this.topImgIndex + 1 : dataLength + this.topImgIndex - 1;
this.topImgIndex = tempIndex % dataLength;
this.marginBottom = 0;
});
}
build() {
Column(){
Row() {
Text("显示图片数量")
Counter() {
Text(this.showCount.toString())
}
.onInc(() => {
this.showCount += 2;
})
.onDec(() => {
if (this.showCount > 3) {
this.showCount -= 2;
}
})
}
Row({space:5}){
Radio({ value: '横向', group: 'radioGroup' })
.myRadioStyle()
.onChange((isChecked: boolean) => {
if(isChecked)this.directionvertical=false
})
Text( '横向')
Radio({ value: '纵向', group: 'radioGroup' })
.myRadioStyle()
.checked(true)
.onChange((isChecked: boolean) => {
if(isChecked)this.directionvertical=true
})
Text( '纵向')
}
Stack({ alignContent: Alignment.Center }){
ForEach(this.galleryList,(item: ResourceStr, index: number)=>{
Stack() {
Image(item)
.objectFit(ImageFit.Cover)
.borderRadius(16)
.opacity(1 - Math.min( Math.floor(this.showCount/2),
Math.abs(this.getImgCoefficients(index))) * 0.1)
Text(index+'').fontSize(40).fontColor(Color.Red)
}
.borderRadius(16)
.offset({ x: this.directionvertical?0:this.getOffSetY(index), y: this.directionvertical?this.getOffSetY(index):0 })
.aspectRatio(this.directionvertical?7/5:5/7) // 宽高比
.width(index != this.topImgIndex && this.getImgCoefficients(index) === 0
? this.directionvertical?'66%':'36%' : `${(this.directionvertical?86:46) - this.offsetCoefficients * Math.abs(this.getImgCoefficients(index))}%`)
.zIndex(index != this.topImgIndex && this.getImgCoefficients(index) === 0
? 0 : Math.floor(this.showCount/2) - Math.abs(this.getImgCoefficients(index)))
.blur(this.offsetCoefficients * Math.abs(this.getImgCoefficients(index)))
})
}.width('100%').height('100%').alignContent(Alignment.Center)
.gesture(
PanGesture({ direction: this.directionvertical?PanDirection.Vertical:PanDirection.Horizontal })
.onActionStart((event: GestureEvent) => {
this.changedIndex = false;
this.handlePanGesture(this.directionvertical?event.offsetY:event.offsetX);
})
.onActionUpdate((event: GestureEvent) => {
this.handlePanGesture(this.directionvertical?event.offsetY:event.offsetX);
})
.onActionEnd(() => {
this.marginBottom = 0
})
)
}.width('100%').height('100%')
}
}
@Extend(Radio)
function myRadioStyle() {
.checked(false)
.radioStyle({
checkedBackgroundColor: Color.Green, //开启状态底板颜色
uncheckedBorderColor:Color.Red, //关闭状态描边颜色
indicatorColor:Color.Yellow //开启状态内部圆饼颜色
})
.height(40)
.width(40)
}