📖

HarmonyOS高度なノウハウ:個性豊かな動的Swiper効果の制作

に公開

まえがき

HarmonyOSの広い分野において、開発者は驚くべきユーザーエクスペリエンスを創造する機会があります。最近、私はユニークなスライディング効果を持つSwiperコンポーネントを設計する作業を始めました。スライド時に視野に素早く入り、古いcellを巧妙に視界の外に隠すことができます。本稿では、HarmonyのSwiperコンポーネントを使用して、この魅力的な動的効果を実現する方法を共有します。

画像の説明

一、デザインと構想

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) => {
      // カスタムトランジションのロジック
    }
  });

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[] = []

Itemのサイズと位置の設定コード:

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

customContentTransitionのtransition属性で設定:

// scaleListは線形変化が必要です
// translateListの移動はデータオフセット処理とベジエ曲線処理が必要です
// zIndexListは位置階層設定が必要です

this.scaleList[proxy.index] = 線形関数
this.translateList[proxy.index] = - proxy.position * proxy.mainAxisLength + ベジエ曲線関数
this.zIndexList[proxy.index] = proxy.position

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;

}

線形関数:

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;

}

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つのコンポーネントに統合し、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
          }
        })
  }

五、おわりに

本稿では、個性豊かな動的効果を持つSwiperコンポーネントを実現するだけでなく、HarmonyOSの強力なAPIを使用してアニメーションとレイアウトをカスタマイズする方法について詳しく解説しました。この記事が他の開発者の創造性を刺激し、HarmonyOSの無限の可能性を一緒に探求するのに役立つことを願っています。

Discussion