Stack组件实现Swiper堆叠动画效果
=====================

#### 问题现象

Swiper如何实现卡片堆叠样式：

*   上下堆叠：Swiper内容像卡片堆叠，在底部留部分空间显示下一页的内容，上下滑实现卡片切换。
*   左右堆叠：Swiper内容像卡片堆叠，在右侧留部分空间显示下一页的内容，左右滑实现卡片切换。

#### 背景知识

*   [Swiper](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-layout-development-create-looping)组件提供滑动轮播显示的能力。Swiper本身是一个容器组件，当设置了多个子组件后，可以对这些子组件进行轮播显示。
*   [Stack](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-stack)是堆叠容器，子组件按照顺序依次入栈，后一个子组件覆盖前一个子组件。
*   [gesture](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-gesture-settings#gesture)可以为组件绑定手势方法进行相应处理，如滑动手势事件[PanGesture](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-gestures-pangesture)。
*   [animateTo](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-uicontext-uicontext#animateto)指定由于闭包代码导致的状态变化插入过渡动效。

#### 解决方案

*   **上下堆叠实现。**
    
    自定义实现卡片堆叠的组件：使用Stack组件堆叠需要展示的图片，设置最上面的图片向上偏移部分距离，露出下一张图片的底部。为Stack绑定上下滑动的手势处理，实现切换图片逻辑，同时使用animateTo接口设置图片切换动画。
    
    export class SwiperData {
      imageSrc: Resource;
    
      constructor(imageSrc: Resource) {
        this.imageSrc \= imageSrc;
      }
    }
    
    @Component
    export struct SwiperStackComponent {
      @Link currentIndex: number;
      @Prop swiperData: SwiperData\[\];
      private halfCount: number \= Math.floor(3 / 2);
      private automaticSlidingDuration: number \= 300;
    
      aboutToAppear(): void {
        this.currentIndex \= 0;
      }
    
      _//_ _修改堆叠方向系数计算_
      getImgCoefficients(index: number): number {
        const coefficient \= this.currentIndex \- index;
        const tempCoefficient \= Math.abs(coefficient);
        if (tempCoefficient <\= this.halfCount) {
          return coefficient;
        }
        const dataLength \= this.swiperData.length;
        let tempOffset \= dataLength \- tempCoefficient;
        if (tempOffset <\= this.halfCount) {
          return coefficient \> 0 ? -tempOffset : tempOffset;
        }
        return 0;
      }
    
      _// 修改堆叠方向偏移量计算_
      getOffSet(index: number): number {
        let offsetIndex \= this.getImgCoefficients(index);
        const tempOffset \= Math.abs(offsetIndex);
        let offset \= 0;
        if (tempOffset \=== 1) {
          if (offsetIndex \=== 1) {
            offsetIndex \= -1;
          }
          offset \= 50 \* offsetIndex;
        }
        return \-offset;
      }
    
      startAnimation(isLeft: boolean, duration: number): void {
        this.getUIContext().animateTo({ duration: duration, }, () \=\> {
          const dataLength: number \= this.swiperData.length;
          const tempIndex: number \= isLeft ? this.currentIndex \+ 1 : this.currentIndex \- 1 \+ dataLength;
          this.currentIndex \= tempIndex % dataLength;
        });
      }
    
      build() {
        Stack() {
          ForEach(this.swiperData, (item: SwiperData, index: number) \=\> {
            Stack({ alignContent: Alignment.Bottom }) {
              Image(item.imageSrc)
                .objectFit(ImageFit.Cover)
                .width('100%')
                .height('100%')
                .borderRadius(8);
            }
            .offset({ x: 0, y: this.getOffSet(index) })
            .shadow(ShadowStyle.OUTER\_DEFAULT\_SM)
            .backgroundColor(Color.White)
            .borderRadius(8)
            .blur(index !== this.currentIndex ? 12 : 0)
          _  // 通过animateTo实现动画并且同时改变currentIndex数据中间值来判断组件zIndex实现切换动画_
            .zIndex(index !== this.currentIndex && this.getImgCoefficients(index) \=== 0 ?
              0 : 2 \- Math.abs(this.getImgCoefficients(index)))
            .width(310)
            .height(index !== this.currentIndex ? 130 : 180);
          });
        }
        .height(200)
        .width('100%')
        .gesture(
          PanGesture({ direction: PanDirection.Vertical })
            .onActionStart((event: GestureEvent) \=\> {
              this.startAnimation(event.offsetY < 0, this.automaticSlidingDuration);
            })
        )
        .alignContent(Alignment.Center)
        .padding({ left: 12, right: 12, top: 12 });
      }
    }
    
    在页面中直接使用上面封装好的SwiperStackComponent即可，示例如下：传给SwiperStackComponent要堆叠的图片。
    
    @Entry
    @Component
    struct StackSwiperDemo {
      @State currentIndex: number \= 0;
     _ // 图片资源需自行配置_
      @State swiperData: SwiperData\[\] \= \[
        new SwiperData($r('app.media.img1')),
        new SwiperData($r('app.media.img2')),
        new SwiperData($r('app.media.img3')),
      \];
    
      build() {
        Column() {
          SwiperStackComponent({
            currentIndex: this.currentIndex,
            swiperData: this.swiperData,
          });
        };
      }
    }
    
    效果预览：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/82/v3/MDjGkCXwTPSS8uRCndGLug/zh-cn_image_0000002628552022.gif?HW-CC-KV=V1&HW-CC-Date=20260709T134030Z&HW-CC-Expire=86400&HW-CC-Sign=4BA8110A63A2EBC5F6C1FD9CD5CA86305866ED3574C2DA39F463A42217BE8D83 "点击放大")
    

*   **左右堆叠实现。**
    
    和上下堆叠实现类似，只需微调SwiperStackComponent代码即可。
    
    1.  将内层Stack在Y方向上的偏移改为X方向上的偏移。
        
        Stack({ alignContent: Alignment.Bottom }) {
          Image(item.imageSrc)
            .objectFit(ImageFit.Cover)
            .width('100%')
            .height('100%')
            .borderRadius(8);
        }
        .offset({ x: this.getOffSet(index), y: 0 }) _//_ _偏移改为X方向上的偏移_
        
    2.  外层Stack的滑动手势改为左右滑动，动画效果改为X方向上的判断。
        
        .gesture(
          _// 滑动手势改为左右滑动_
          PanGesture({ direction: PanDirection.Horizontal })
            .onActionStart((event: GestureEvent) \=\> {
           _   // 改成X轴方向判定_
              this.startAnimation(event.offsetX < 0, this.automaticSlidingDuration);
            })
        )
        
    3.  由于屏幕X方向比Y方向要窄，可以修改getOffSet函数改变偏移的距离。
        
        offset \= 20 \* offsetIndex; _//_ _改变偏移的距离_
        
    
    效果预览：
    
    ![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/f6/v3/VXw9uYg-TGGFtErOCrDlPA/zh-cn_image_0000002658911343.gif?HW-CC-KV=V1&HW-CC-Date=20260709T134030Z&HW-CC-Expire=86400&HW-CC-Sign=24833DBC41F59D8C6C5E21681D836A0641688582FAFA8568EB702B4C5CF2FCAE "点击放大")
    

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/f0/v3/m-I4spP2RMKTlLR1SfjrIQ/note_3.0-zh-cn.png?HW-CC-KV=V1&HW-CC-Date=20260709T134030Z&HW-CC-Expire=86400&HW-CC-Sign=DB087302C6A912C3C6895CABECC4465D5FAEA7AEE8823BDC98CC29068195EB86)

堆叠方向的偏移量可根据需求，通过修改自定义组件SwiperStackComponent的getImgCoefficients函数和getOffSet函数来调整。