如何实现商品切换与图片3D立体轮播效果
===================

#### 问题现象

问题一：如何实现商品切换的动画效果？该动画常用于商城首页商品展示场景。

问题二：如何实现多张图片3D立体轮播动效？

#### 背景知识

*   [Swiper](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-reusable#swiper使用场景)：滑块视图容器，提供子组件滑动轮播显示的能力。
*   [Stack](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-layout-development-stack-layout)：堆叠容器，子组件按照顺序依次入栈，后一个子组件覆盖前一个子组件。
*   [opacity](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-arkui-rendernode#opacity)：设置组件的透明度。
*   [scale](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-transformation#scale)：设置组件缩放。
*   [setInterval](https://developer.huawei.com/consumer/cn/doc/atomic-ascf/apis-timer#setinterval)：重复调用一个函数，在每次调用之间具有固定的时间延迟。

#### 解决方案

*   问题一：
    *   方式一：使用Swiper组件，将商品图两两分组，按组翻页，实现两两切换的效果。
        
        参考[示例4（设置自定义页面切换动画）](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-swiper#示例4设置自定义页面切换动画)。
        
    *   方式二：使用Stack布局，结合opacity和scale属性，设置定时器，自定义图片切换的方法，实现问题现象中的效果。
        
        @Entry
        @Component
        struct ProductSwitch {
          imgArr: AnimateModel\[\] = \[\];
          @State topImg: AnimateModel | undefined = undefined;
          @State bottomImg: AnimateModel | undefined = undefined;
          index: number = 0;
          @State imageOpacity1: number = 1;
          @State imageOpacity2: number = 0;
          @State imageScale1: number = 1;
          @State imageScale2: number = 0.6;
          private timer: number | null = null;
          aboutToDisappear(): void{
            if (this.timer !== null) {
              clearInterval(this.timer); _// 清理定时器_
              this.timer = null;
            }
          };
          aboutToAppear(): void {
            _// 此处'app.media.startIcon'等资源仅作示例，请开发者自行替换，否则image创建失败会导致后续无法正常执行。_
            this.imgArr.push(new AnimateModel($r('app.media.startIcon'),
              $r('app.media.startIcon')));
            this.imgArr.push(new AnimateModel($r('app.media.startIcon'),
              $r('app.media.startIcon')));
            this.imgArr.push(new AnimateModel($r('app.media.startIcon'),
              $r('app.media.startIcon')));
            this.imgArr.push(new AnimateModel($r('app.media.startIcon'),
              $r('app.media.startIcon')));
            this.topImg = this.imgArr\[this.index\];
            this.bottomImg = this.imgArr\[this.index+1\];
            _// 定时器执行图片切换_
            this.timer = setInterval(() => {
              this.getUIContext().animateTo({
                duration: 500, onFinish: () => {
                  if (++this.index === this.imgArr.length) {
                    this.index = 0;
                  }
                  let tempIndex = this.index + 1 === this.imgArr.length ? 0 : this.index + 1;
                  if (this.imageOpacity1 === 1) {
                    this.bottomImg = this.imgArr\[tempIndex\];
                  } else {
                    this.topImg = this.imgArr\[tempIndex\];
                  }
                }
              }, () => {
                this.imageOpacity1 = this.imageOpacity1 ? 0 : 1; _// 透明度和大小变化_
                this.imageOpacity2 = this.imageOpacity2 ? 0 : 1;
                this.imageScale1 = this.imageScale1 === 1 ? 0.6 : 1;
                this.imageScale2 = this.imageScale2 === 1 ? 0.6 : 1;
              });
            }, 2000);
          };
        
          build() {
            Row({ space: 20 }) {
              Stack() {
                Image(this.topImg?.leftImg)
                  .width(100)
                  .opacity(this.imageOpacity1)   _// 图片透明度_
                  .scale({ x: this.imageScale1, y: this.imageScale1 }) _// 图片大小_
                Image(this.bottomImg?.leftImg)
                  .width(100)
                  .opacity(this.imageOpacity2)
                  .scale({ x: this.imageScale2, y: this.imageScale2 })
              }
        
              Stack() {
                Image(this.topImg?.rightImg)
                  .width(100)
                  .opacity(this.imageOpacity1)
                  .scale({ x: this.imageScale1, y: this.imageScale1 })
                Image(this.bottomImg?.rightImg)
                  .width(100)
                  .opacity(this.imageOpacity2)
                  .scale({ x: this.imageScale2, y: this.imageScale2 })
              }
            }
            .height('100%')
            .width('100%')
            .justifyContent(FlexAlign.Center)
          }
        }
        
        class AnimateModel {
          leftImg: Resource;
          rightImg: Resource;
        
          constructor(leftImg: Resource, rightImg: Resource) {
            this.leftImg = leftImg;
            this.rightImg = rightImg;
          }
        };
        
*   问题二：
    
    使用List方式实现类似Swiper效果。通过设置scrollSnapAlign和listDirection属性实现横向居中对齐滚动，并在滚动时动态计算卡片的旋转角度和宽度，实现旋3D立体轮播效果。
    
    class SwiperData {
      imageSrc: Resource;
      constructor(imageSrc: Resource) {
        this.imageSrc = imageSrc;
      }
    }
    
    @Entry
    @Component
    struct StackSwiperDemo {
      @State centerIndex: number = 4;
      _// 图片资源需自行配置_
      @State swiperData: SwiperData\[\] = \[
        new SwiperData($r('app.media.startIcon')),
        new SwiperData($r('app.media.startIcon')),
        new SwiperData($r('app.media.startIcon')),
        new SwiperData($r('app.media.startIcon')),
        new SwiperData($r('app.media.startIcon')),
        new SwiperData($r('app.media.startIcon')),
        new SwiperData($r('app.media.startIcon')),
      \];
    
      _// 返回每个卡片的不同旋转角度_
      getAngle(index: number): number {
        return this.centerIndex === index ? 0 : (this.centerIndex > index ? -15 : 15);
      }
    
      _// 返回每个卡片的不同宽度_
      getWidth(index: number): string {
        if (this.centerIndex === index) {
          return '40%';
        }
        if (this.centerIndex === index + 1 || this.centerIndex === index - 1) {
          return '20%';
        }
        return '10%';
      }
    
      build() {
        Row() {
          List() {
            ForEach(this.swiperData, (item: SwiperData, index: number) => {
              ListItem() {
                Row() {
                  Image(item.imageSrc)
                    .width(this.getWidth(index))
                    .rotate({
                      y: 1,
                      angle: this.getAngle(index),
                      perspective: 5
                    })
                    .height(300)
                    .animation({
                      duration: 500,
                      curve: Curve.EaseOut,
                      iterations: 1,
                      playMode: PlayMode.Normal
                    });
                }
                .height(400)
                .justifyContent(FlexAlign.Center);
              };
            }, (item: string) => item);
          }
          .scrollSnapAlign(ScrollSnapAlign.CENTER) _// 中间项对齐_
          .listDirection(Axis.Horizontal) _// List组件设置横向滚动_
          .scrollBar(BarState.Off)
          .onScrollIndex((firstIndex: number, lastIndex: number, centerIndex: number) => {
            _// 获取屏幕中间item的索引值_
            this.centerIndex = centerIndex;
            console.info(\`firstIndex：${firstIndex},lastIndex：${lastIndex}\`);
          })
          .width('100%');
        }
        .width('100%')
        .height(400)
        .backgroundColor(0xDCDCDC);
      }
    }