DEV Community

HarmonyOS
HarmonyOS

Posted on

Customized Swiper Transition

Read the original article:Customized Swiper Transition

Customized Swiper Transition

Context

This example demonstrates a customized Swiper transition in ArkUI, where each page changes its text opacity and background color smoothly as the user swipes horizontally. The implementation focuses on visual continuity - the active page gradually fades out while the net page fades in, and the background color transitions softly between predefined RGB values.

The entier behavior is handled within a single component using state variables, without any additional libraries or complex animations.

Description

The SwiperPageExamples component defines three pages, Home, About and Contact to demonstrate this example. Each page has its own background color and text elements that react to swipe gestures. The code listens to the user's swipe movement through the onTouch event of the Swiper component. When a horizontal drag is detected, the movement ratio (progress) is calculated based on the finger's distance relative to the screen width. This ratio is then used to interpolate both color blending and opacity transitions in real time. To prevent accidental transitions during vertical movements, the component ignores swipes where the vertical distance is greater than the horizontal one.

Solution

The key functions that control the behavior are shown below:

1.Text Opacity Control

private getOpacity(index: number): number {
  if (index === this.currentIndex && index === this.nextIndex)
    return 1;

  if (index === this.currentIndex) {
    // Current page fades out (1 → 0.3)
    return 1 - (this.progress * 0.7);
  }

  if (index === this.nextIndex) {
    // Next page fades in (0.3 → 1)
    return 0.3 + (this.progress * 0.7);
  }

  // Inactive pages stay dim
  return 0.3;
}
Enter fullscreen mode Exit fullscreen mode

2.Background Color Blending

private getBlendedColor(index: number): string {
  if (index === this.currentIndex || index === this.nextIndex) {
    const from: RGB = this.pageColors[this.currentIndex];
    const to: RGB = this.pageColors[this.nextIndex];
    const alpha = 255; // Opaque background
    return this.blendToHex(from, to, this.progress, alpha);
  }

  const rgb: RGB = this.pageColors[index];
  // Slightly desaturate inactive pages
  return this.rgbToHex(this.desaturateColor(rgb, 0.3), 255);
}
Enter fullscreen mode Exit fullscreen mode

3.Swipe Tracking Logic

.onTouch((e: TouchEvent) => {
  if (e.type === TouchType.Down) {
    this.dragging = true;
    this.dragStartX = e.touches[0].x;
    this.dragStartY = e.touches[0].y;
  }
  else if (e.type === TouchType.Move && this.dragging) {
    const dx: number = e.touches[0].x - this.dragStartX;
    const dy: number = e.touches[0].y - this.dragStartY;

    // Ignore vertical gestures
    if (Math.abs(dy) > Math.abs(dx)) return;

    // Calculate horizontal swipe ratio
    const ratio: number = Math.min(Math.max(Math.abs(dx) / this.screenWidthVp, 0), 1);
    this.progress = ratio;
    this.nextIndex = dx < 0
      ? Math.min(this.currentIndex + 1, this.pageColors.length - 1)
      : Math.max(this.currentIndex - 1, 0);
  }
  else if (e.type === TouchType.Up || e.type === TouchType.Cancel) {
    this.dragging = false;
    this.progress = 0;
  }
})
Enter fullscreen mode Exit fullscreen mode

Each function plays a specific role:

getOpacity handles fading, getBlendedColor handles color interpolation, and the onTouch event dynamically updates the transition progress.

Full Implementation:

import display from '@ohos.display';

type RGB = [number, number, number];

@Entry
@Component
struct SwiperPageExamples {
  private swiperController: SwiperController = new SwiperController();

  @State currentIndex: number = 0;
  @State nextIndex: number = 0;
  @State progress: number = 0;
  @State dragging: boolean = false;

  private dragStartX: number = 0;
  private dragStartY: number = 0;
  private screenWidthVp: number = 360;

  // Page background colors (RGB format)
  private pageColors: RGB[] = [
    [0, 122, 255],   // Blue
    [0, 180, 80],    // Green
    [255, 200, 0]    // Yellow
  ];

  aboutToAppear(): void {
    // Get screen width in VP for progress ratio calculation
    const d = display.getDefaultDisplaySync();
    this.screenWidthVp = this.getUIContext().px2vp(d.width);
  }

  build() {
    Column({ space: 10 }) {
      Swiper(this.swiperController) {
        // --- Page 1 ---
        Column() {
          Text('Home Page')
            .fontSize(24).fontWeight(FontWeight.Bold).margin(10)
            .opacity(this.getOpacity(0))
          Text('This is the first page').fontSize(16)
            .opacity(this.getOpacity(0))
          Button('Next').margin({ top: 20 })
            .opacity(this.getOpacity(0))
            .onClick(() => this.swiperController.showNext())
        }
        .width('100%').height('100%')
        .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        .backgroundColor(this.getBlendedColor(0))

        // --- Page 2 ---
        Column() {
          Text('About Page')
            .fontSize(24).fontWeight(FontWeight.Bold).margin(10)
            .opacity(this.getOpacity(1))
          Text('This is the second page').fontSize(16)
            .opacity(this.getOpacity(1))
          Button('Go Back').margin({ top: 20 })
            .opacity(this.getOpacity(1))
            .onClick(() => this.swiperController.showPrevious())
        }
        .width('100%').height('100%')
        .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        .backgroundColor(this.getBlendedColor(1))

        // --- Page 3 ---
        Column() {
          Text('Contact Page')
            .fontSize(24).fontWeight(FontWeight.Bold).margin(10)
            .opacity(this.getOpacity(2))
          Text('This is the third page').fontSize(16)
            .opacity(this.getOpacity(2))
          Button('Back to start').margin({ top: 20 })
            .opacity(this.getOpacity(2))
            .onClick(() => this.swiperController.changeIndex(0))
        }
        .width('100%').height('100%')
        .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
        .backgroundColor(this.getBlendedColor(2))
      }
      .indicator(true)
      .width('100%').height('100%')

      // Handle page change event
      .onChange((i: number) => {
        this.currentIndex = i;
        this.nextIndex = i;
        this.progress = 0;
      })

      // Track swipe gestures (for smooth transition)
      .onTouch((e: TouchEvent) => {
        if (e.type === TouchType.Down) {
          this.dragging = true;
          this.dragStartX = e.touches[0].x;
          this.dragStartY = e.touches[0].y; // Record Y starting position
        }
        else if (e.type === TouchType.Move && this.dragging) {
          const dx: number = e.touches[0].x - this.dragStartX;
          const dy: number = e.touches[0].y - this.dragStartY;

          // Ignore vertical swipe gestures
          if (Math.abs(dy) > Math.abs(dx)) {
            return;
          }

          // Update progress ratio only for horizontal swipes
          const ratio: number = Math.min(Math.max(Math.abs(dx) / this.screenWidthVp, 0), 1);
          this.progress = ratio;
          this.nextIndex = dx < 0
            ? Math.min(this.currentIndex + 1, this.pageColors.length - 1)
            : Math.max(this.currentIndex - 1, 0);
        }
        else if (e.type === TouchType.Up || e.type === TouchType.Cancel) {
          this.dragging = false;
          this.progress = 0;
        }
      })
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
  }

  // --- Control text opacity based on swipe progress ---
  private getOpacity(index: number): number {
    if (index === this.currentIndex && index === this.nextIndex)
      return 1;

    if (index === this.currentIndex) {
      // Current page: fade out gradually (1 → 0.3)
      return 1 - (this.progress * 0.7);
    }

    if (index === this.nextIndex) {
      // Next page: fade in gradually (0.3 → 1)
      return 0.3 + (this.progress * 0.7);
    }

    // Inactive pages: always low opacity
    return 0.3;
  }

  // --- Calculate smooth blended background color ---
  private getBlendedColor(index: number): string {
    if (index === this.currentIndex || index === this.nextIndex) {
      const from: RGB = this.pageColors[this.currentIndex];
      const to: RGB = this.pageColors[this.nextIndex];

      // Blend active and next colors
      const alpha = 255; // Fully opaque background
      return this.blendToHex(from, to, this.progress, alpha);
    }

    const rgb: RGB = this.pageColors[index];
    // Desaturate inactive pages for a dimmed look
    return this.rgbToHex(this.desaturateColor(rgb, 0.3), 255);
  }

  // --- Desaturate color (make it look dimmer) ---
  private desaturateColor(rgb: RGB, factor: number): RGB {
    const gray = (rgb[0] + rgb[1] + rgb[2]) / 3;
    return [
      Math.round(rgb[0] + (gray - rgb[0]) * factor),
      Math.round(rgb[1] + (gray - rgb[1]) * factor),
      Math.round(rgb[2] + (gray - rgb[2]) * factor)
    ];
  }

  // --- Convert RGB to hex color string (#AARRGGBB) ---
  private rgbToHex(rgb: RGB, a: number): string {
    const r: number = this.clamp255(rgb[0]);
    const g: number = this.clamp255(rgb[1]);
    const b: number = this.clamp255(rgb[2]);
    const aa: number = this.clamp255(a);
    return (
      '#' +
      aa.toString(16).padStart(2, '0') +
      r.toString(16).padStart(2, '0') +
      g.toString(16).padStart(2, '0') +
      b.toString(16).padStart(2, '0')
    ).toUpperCase();
  }

  // --- Blend two RGB colors based on ratio ---
  private blendToHex(a: RGB, b: RGB, t: number, alpha: number): string {
    const r: number = Math.round(a[0] + (b[0] - a[0]) * t);
    const g: number = Math.round(a[1] + (b[1] - a[1]) * t);
    const bch: number = Math.round(a[2] + (b[2] - a[2]) * t);
    return this.rgbToHex([r, g, bch], alpha);
  }

  // --- Clamp color channel values between 0–255 ---
  private clamp255(v: number): number {
    return Math.max(0, Math.min(255, Math.round(v)));
  }
}
Enter fullscreen mode Exit fullscreen mode

Code Running Effect Diagram:

cke_7799.png

Key Takeaways

  • Smooth UI effects can be achieved by combining onTouch tracking with simple math interpolation.
  • Opacity and color blending can run in real time without using animation libraries.
  • The Swiper component supports dynamic, touch-driven transitions with minimal state management.
  • This lightweight pattern is ideal for clean, responsive interfaces in ArkUI.

Written by Recep Sadullah Yegin

Top comments (0)