Swiper如何实现3D立方体旋转切换动画效果
=======================

#### 问题现象

通常Swiper组件，提供平面滑动轮播显示的效果，如何通过Swiper组件实现对子组件3D立方体旋转切换动画效果？

#### 效果预览

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/a/v3/97ClRHuASkWB4B2kH_GkUQ/zh-cn_image_0000002628400290.gif?HW-CC-KV=V1&HW-CC-Date=20260709T134040Z&HW-CC-Expire=86400&HW-CC-Sign=B7EB1CBABC3047A9FE77A0A393CFF6CC9FE06420A7DAF5FBACAB53BF566B7848 "点击放大")

#### 背景知识

*   [Swiper组件](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-swiper)：滑块视图容器组件，它提供了子组件滑动轮播显示的能力。
*   [customContentTransition属性](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-swiper#customcontenttransition12)：用于实现自定义的过渡动画效果，该属性允许开发者通过回调函数动态控制轮播切换过程中的动画细节，特别是可结合进度参数进行精细化动画控制。
*   [rotate属性](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-transformation#rotate)：主要用于设置组件的旋转，可使组件在以组件左上角为坐标原点的坐标系中进行旋转。其中，（x，y，z）指定一个矢量，作为旋转轴。旋转轴和旋转中心点都基于坐标系设定，组件发生位移时，坐标系不会随之移动。默认值：在x、y、z都不指定时，x、y、z的默认值分别为0、0、1。指定了x、y、z任何一个值时，x、y、z中未指定的值默认为0。

#### 解决方案

1.  给Swiper组件内的子组件设置旋转属性rotate。
    
    Stack() {
      this.swiperItemSlotParam(item);
    }
    _//_ _设置组件旋转_
    .rotate({
      x: 0,
      y: 1,
      z: 0,
      angle: this.angleList\[index\],
      centerX: this.centerXList\[index\],
      centerY: '50%',
      centerZ: 0,
      perspective: 0
    });
    
2.  给Swiper组件设置页面切换动画属性customContentTransition，在页面切换时逐帧触发回调，在回调中设置子组件的rotate属性值。
    
    _//_ _自定义Swiper页面切换动画_
    .customContentTransition({
     _ // 页面移除视窗时超时1000ms下渲染树_
      timeout: 1000,
      transition: (proxy: SwiperContentTransitionProxy) \=\> {
       _ // 旋转角度_
        let angle \= 0;
        console.info('proxy.position===\>' \+ proxy.position);
        console.info('proxy.index===\>' \+ proxy.index);
       _ // position为index页面相对于selectedIndex对应页面的起始位置的移动比例，向左移动减小，向右移动增加。_
        if (proxy.position < 0 && proxy.position \> -1) {
         _ // 当前页向左滑出或上一页向右滑入_
          angle \= proxy.position \* 90;
       _   // 设置index页面的旋转中心轴为右侧边缘_
          this.centerXList\[proxy.index\] \= '100%';
        } else if (proxy.position \> 0 && proxy.position < 1) {
        _  // 当前页向右滑出或下一页向左滑入_
          angle \= proxy.position \* 90;
         _ // 设置index页面的旋转中心轴为左侧边缘_
          this.centerXList\[proxy.index\] \= '0%';
        } else {
       _   // position小于\-1时表示向左完全滑出区域，大于1时表示向右完全滑出区域，重置角度_
          angle \= 0;
        }
      _  // 修改index页的旋转角_
        this.angleList\[proxy.index\] \= angle;
      }
    });
    

完整示例参考如下：

@Component
@Entry
export struct Swiper3D {
 _ // Swiper数据_
  private swiperList: MySwiperItem\[\] \=
    \[new MySwiperItem('模块1', '#4B48F7'),
      new MySwiperItem('模块2', '#46B1E3'),
      new MySwiperItem('模块3', '#61CFBE')\]
  ;

  build() {
    Column() {
     _ // 轮播网格_
      Custom3DComponentPage({
        items: this.swiperList,
        swiperItemSlotParam: (item: MySwiperItem) \=\> {
          this.mySwiperItem(item);
        }
      });
    };
  }

 _ // 自定义3D立方体旋转轮播项UI内容_
  @Builder
  mySwiperItem(item: MySwiperItem) {
    Column() {
      Text(item.title).fontSize(24);
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .backgroundColor(item.colors);
  }
}

export class MySwiperItem {
 _ // 标题_
  title: string;
 _ // 颜色_
  colors: Color | string;

  constructor(title: string, colors: Color | string) {
    this.title \= title;
    this.colors \= colors;
  }
}

@Component
export struct Custom3DComponentPage {
  _// --------------------暴露外部属性\----------------------------_
  _// 动画持续时间，默认500ms_
  duration: number \= 500;
 _ // 是否自动播放_
  autoPlay: boolean \= false;
 _ // 是否循环播放_
  loop: boolean \= true;
 _ // 轮播数据_
  items: ESObject\[\] \= \[\];
 _ // 轮播页插槽参数_
  @BuilderParam swiperItemSlotParam: (item: ESObject) \=\> void;
 _ // --------------------私有属性\----------------------------_
  _// 当前项下标_
  @State currentIndex: number \= 0;
  _//_ _旋转角度列表_
  @State angleList: number\[\] \= \[\];
  _// 旋转中心点列表_
  @State centerXList: Array<number | string\> = \[\];
  _// 轮播控制器_
  private swiperController: SwiperController \= new SwiperController();

  build() {
    Swiper(this.swiperController) {
      ForEach(this.items, (item: ESObject, index: number) \=\> {
        Stack() {
          this.swiperItemSlotParam(item);
        }
       _ // 设置组件旋转_
        .rotate({
          x: 0,
          y: 1,
          z: 0,
          angle: this.angleList\[index\],
          centerX: this.centerXList\[index\],
          centerY: '50%',
          centerZ: 0,
          perspective: 0
        });

      }, ((item: ESObject, index: number) \=\> \`${JSON.stringify(item)}\_${index}\`));
    }
    .loop(this.loop)
    .autoPlay(this.autoPlay)
    .duration(this.duration)
    .onChange((index: number) \=\> {
      this.currentIndex \= index;
    })
   _ // 自定义Swiper页面切换动画_
    .customContentTransition({
      _// 页面移除视窗时超时1000ms下渲染树_
      timeout: 1000,
      transition: (proxy: SwiperContentTransitionProxy) \=\> {
    _    // 旋转角度_
        let angle \= 0;
        console.info('proxy.position===\>' \+ proxy.position);
        console.info('proxy.index===\>' \+ proxy.index);
       _ // position为index页面相对于selectedIndex对应页面的起始位置的移动比例，向左移动减小，向右移动增加。_
        if (proxy.position < 0 && proxy.position \> -1) {
          _//_ _当前页向左滑出或上一页向右滑入_
          angle \= proxy.position \* 90;
        _  // 设置index页面的旋转中心轴为右侧边缘_
          this.centerXList\[proxy.index\] \= '100%';
        } else if (proxy.position \> 0 && proxy.position < 1) {
       _   // 当前页向右滑出或下一页向左滑入_
          angle \= proxy.position \* 90;
        _  // 设置index页面的旋转中心轴为左侧边缘_
          this.centerXList\[proxy.index\] \= '0%';
        } else {
       _   // position小于\-1时表示向左完全滑出区域，大于1时表示向右完全滑出区域，重置角度_
          angle \= 0;
        }
       _ // 修改index页的旋转角_
        this.angleList\[proxy.index\] \= angle;
      }
    });
  }
}