前言

在鸿蒙OS的广阔天地中,开发者们有机会创造出令人惊叹的用户体验。最近,我着手设计一款具有独特滑动效果的Swiper组件,它在滑动时能够迅速进入视野,同时巧妙地将旧的cell隐藏到视线之外。本文将分享如何利用鸿蒙的Swiper组件,实现这一引人入胜的动态效果。

鸿蒙OS高级技巧:打造个性化动态Swiper效果_sed

一、设计与构思

Swiper的设计理念是简洁而富有动感。每个cell在滑动时不仅会逐渐缩小至原始大小的70%,还会被前一个cell覆盖,创造出一种流畅且连续的视觉效果。这种效果的实现,依赖于精确的动画控制和布局调整。

二、代码设计与实现思路

实现这一效果,我们需要对Swiper组件进行深度定制。这包括对cell的尺寸、位置和层级进行动态调整,以及利用贝塞尔曲线来实现平滑的动画效果。

三、控件采用与代码说明
3.1 Swiper组件定制

Swiper组件提供了丰富的API,允许我们对其行为进行精细控制。以下是一些关键的配置项和它们的作用:

  • itemSpace: 控制cell之间的间距。
  • indicator: 是否显示指示器。
  • displayCount: 设置同时展示的cell数量。
  • onAreaChange: 当Swiper区域大小变化时的回调。
  • customContentTransition: 自定义内容转换动画。

Swiper组件基础配置代码:

Swiper()
  .itemSpace(12)
  .indicator(false)
  .displayCount(this.DISPLAY_COUNT)
  .padding({left:10, right:10})
  .onAreaChange((oldValue, newValue) => {
    // 处理区域变化逻辑
  })
  .customContentTransition({
    transition: (proxy) => {
      // 自定义转换逻辑
    }
  });
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
3.2 Item组件设置

每个Item需要根据其在Swiper中的位置进行尺寸、位置和层级的调整。这涉及到初始化相关变量,并在aboutToAppear生命周期方法中进行设置。

初始化宽高,初始化组件数据:

@State cw: number = 0;
  @State ch: number = 0;

  aboutToAppear(): void {
    initSwipe(...)
  }

  initSwipe(num:number){
    this.translateList = []
    for (let i = 0; i < num; i++) {
      this.scaleList.push(0.8)
      this.translateList.push(0.0)
      this.zIndexList.push(0)
    }
  }
  private MIN_SCALE: number = 0.70
  private DISPLAY_COUNT: number = 4
  private DISPLAY_WIDTH: number = 200
  @State scaleList: number[] = []
  @State translateList: number[] = []
  @State zIndexList: number[] = []
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

Item尺寸和位置设置代码:

LifeStyleItem({lifeStyleResponse: item})
  .scale({ x: this.scaleList[index], y: this.scaleList[index] })
  .translate({ x: this.translateList[index] })
  .zIndex(this.zIndexList[index]);
  • 1.
  • 2.
  • 3.
  • 4.

在 customContentTransition的transition 属性中设置属性:

//scaleList 需要进行线性变化
//translateList 位移需要进行 数据偏移处理和贝塞尔曲线处理
//zIndexList 需要进行位置层级设置

this.scaleList[proxy.index] = 线性函数
this.translateList[proxy.index] = - proxy.position * proxy.mainAxisLength + 贝塞尔曲线函数
this.zIndexList[proxy.index] = proxy.position
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
3.3 自定义动画效果

为了实现平滑的动画效果,我们定义了三次贝塞尔曲线函数和线性函数。这些函数将用于计算cell在滑动过程中的尺寸、位置和层级变化。

三次贝塞尔曲线函数:

function cubicBezier8(t, a1, b1, a2, b2) {
  // 计算三次贝塞尔曲线的值

const k1 = 3 * a1;
const k2 = 3 * (a2 - b1) - k1;
const k3 = 1 - k1 - k2;
return k3 * Math.pow(t, 3) + k2 * Math.pow(t, 2) + k1 * t;

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

线性函数:

function chazhi(startPosition, endPosition, startValue, endValue, position) {
  // 计算线性插值的结果


const range = endPosition - startPosition;
const positionDifference = position - startPosition;
const fraction = positionDifference / range;

const valueRange = endValue - startValue;
const result = startValue + (valueRange * fraction);

return result;

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
3.4 计算函数实现

我们编写了计算函数来确定cell在Swiper中的最终表现。这包括根据位置计算尺寸、位置和层级。

计算尺寸和位置的函数:

function calculateValue(width: number, position: number): number {
  const minValue = 0;

  const normalizedPosition = position / 4;

  // 计算贝塞尔曲线的缓动值
  const easedPosition = cubicBezier(normalizedPosition, 0.3, 0.1, 1,  0.05);

  // 根据缓动值计算最终的变化值
  const value = minValue + (width - minValue) * easedPosition;
  return value;
}

function calculateValueScale(position) {

if (position >= 2.5) {
  // 当position大于2时,值固定为0.8
  return 0.8;
} else if (position < 2.5) {
  const startPosition = 2.5;
  const endPosition = -1;
  // 定义返回值的起始值和结束值
  const startValue = 0.8;
  const endValue = 0.7;
  return chazhi(startPosition,endPosition,startValue,endValue,position)
}

return 0.7;

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
四、全部代码整合

将上述所有代码片段整合到一个组件中,确保Swiper和每个Item都能够根据用户的滑动操作动态调整。 代码如下:

function calculateValue(width: number, position: number): number {
  const minValue = 0;
  const normalizedPosition = position / 4;
  const easedPosition = cubicBezier8(normalizedPosition, 0.3, 0.1, 1,  0.05);
  const value = minValue + (width - minValue) * easedPosition;
  return value;
}

function cubicBezier(t: number, a1: number, b1: number, a2: number, b2: number): number {
  const k1 = 3 * a1;
  const k2 = 3 * (a2 - b1) - k1;
  const k3 = 1 - k1 - k2;
  return k3 * Math.pow(t, 3) + k2 * Math.pow(t, 2) + k1 * t;
}

function calculateValueScale(position: number): number {
  if (position >= 2.5) {
    return 0.8;
  } else if (position < 2.5) {
    const startPosition = 2.5;
    const endPosition = -1;
    const startValue = 0.8;
    const endValue = 0.7;
    return chazhi(startPosition,endPosition,startValue,endValue,position)
  }
  return 0.7;
}

function chazhi(startPosition:number,endPosition:number,startValue:number,endValue:number,position:number):number{

  const range = endPosition - startPosition;
  const positionDifference = position - startPosition;
  const fraction = positionDifference / range;

  const valueRange = endValue - startValue;
  const result = startValue + (valueRange * fraction);

  return result;
}

@Component
struct Banner {
  @State cw: number = 0;
  @State ch: number = 0;
  aboutToAppear(): void {
  initSwipe()
  }

  initSwipe(num:number){
    this.translateList = []
    for (let i = 0; i < num; i++) {
      this.scaleList.push(0.8)
      this.translateList.push(0.0)
      this.zIndexList.push(0)
    }
  }
  private MIN_SCALE: number = 0.70
  private DISPLAY_COUNT: number = 4
  private DISPLAY_WIDTH: number = 200
  @State scaleList: number[] = []
  @State translateList: number[] = []
  @State zIndexList: number[] = []


  build(){
  Swiper() {
          ForEach(this.lifeStyleList, (item: LifeStyleResponse|null,index) => {
            LifeStyleItem({lifeStyleResponse:item})
              .scale({ x: this.scaleList[index], y: this.scaleList[index] })
              .translate({ x: this.translateList[index] })
              .zIndex(this.zIndexList[index])
          }
          )
        }
        .itemSpace(12)
        .indicator(false)
        .displayCount(this.DISPLAY_COUNT)
        .padding({left:10,right:10})
        .onAreaChange((oldValue,newValue)=>{
          this.cw = new Number(newValue.width).valueOf()
          this.ch = new Number(newValue.height).valueOf()
        })
        .customContentTransition({
          transition :(proxy: SwiperContentTransitionProxy)=>{
            this.scaleList[proxy.index] = calculateValueScale(proxy.position)
            this.translateList[proxy.index] = - proxy.position * proxy.mainAxisLength + calculateValue8(this.cw,proxy.position)
            this.zIndexList[proxy.index] = proxy.position
          }
        })
  }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
五、总结

通过本文的深入解析,我们不仅实现了一个具有个性化动态效果的Swiper组件,还学习了如何利用鸿蒙OS的强大API来定制动画和布局。希望这篇文章能够激发更多开发者的创造力,共同探索鸿蒙OS的无限可能。