VOOZH about

URL: https://dev.to/harmonyos/visible-edge-card-carousel-with-swiper-arkui-prevmarginnextmargin-scale-transition-51a4

⇱ Visible-Edge Card Carousel with Swiper (ArkUI) — prevMargin/nextMargin + Scale Transition - DEV Community


Read the original article:Visible-Edge Card Carousel with Swiper (ArkUI) — prevMargin/nextMargin + Scale Transition

Visible-Edge Card Carousel with Swiper (ArkUI) — prevMargin/nextMargin + Scale Transition

Requirement Description

Implement a Swiper that shows a peek of the previous/next items on the current page and applies a scale animation during swipe—i.e., a card-carousel effect.

Background Knowledge

Event order: onGestureSwipeonAnimationStartonChangeonAnimationEnd.

Implementation Steps

Approach A — Event Combo (gesture + animation lifecycle)

  1. Set prevMargin/nextMargin so neighbors are partially visible.
  2. Track drag distance in onGestureSwipe and compute current/prev/next scales.
  3. In onAnimationStart, snap scales to MAX for target and MIN for neighbors.
  4. Update currentIndex in onChange.
  5. Reset helpers in onAnimationEnd.

Approach B — customContentTransition (single place)

  1. Provide a data source and initial scaleArray.
  2. In onChange, mark the selected page as MAX and neighbors as MIN.
  3. In customContentTransition.transition(proxy), compute per-frame current/next/prev scale from proxy.selectedIndex/index/position/mainAxisLength.

Code Snippet / Configuration

Event Combo (condensed)

const MAX_SCALE = 0.7;
const MIN_SCALE = 0.5;
const DRAGGING_MAX_DISTANCE = 1000;
const PAGE_DURATION = 100;
const SWIPER_DURATION = 500;
const CARD_COUNT = 6;

@Entry
@Component
struct CardCarousel {
 private ctrl: SwiperController = new SwiperController();
 @State currentIndex: number = 0;
 @State scaleArray: number[] = new Array(CARD_COUNT).fill(MIN_SCALE);
 private colorArray: Color[] = [Color.Yellow, Color.Blue, Color.Green, Color.Red, Color.Gray, Color.Orange];
 private startSwiperOffset: number = 0;

 aboutToAppear() {
 this.scaleArray[0] = MAX_SCALE;
 }

 private getNextIndex(index: number): number {
 return (index + 1) % CARD_COUNT;
 }

 private getPrevIndex(index: number): number {
 return (index - 1 + CARD_COUNT) % CARD_COUNT;
 }

 private onGestureSwipe(index: number, e: SwiperAnimationEvent) {
 if (this.startSwiperOffset === 0) {
 this.startSwiperOffset = e.currentOffset;
 }

 const distance = Math.abs(this.startSwiperOffset - e.currentOffset);
 const delta = Math.min(distance / DRAGGING_MAX_DISTANCE, MAX_SCALE - MIN_SCALE);
 const nextIndex = this.getNextIndex(index);
 const prevIndex = this.getPrevIndex(index);

 this.scaleArray[index] = MAX_SCALE - delta;

 if (e.currentOffset < this.startSwiperOffset) {
 this.scaleArray[nextIndex] = MIN_SCALE + delta;
 this.scaleArray[prevIndex] = MIN_SCALE;
 } else {
 this.scaleArray[prevIndex] = MIN_SCALE + delta;
 this.scaleArray[nextIndex] = MIN_SCALE;
 }
 }

 private onAnimationStart(_: number, targetIndex: number) {
 this.scaleArray = this.scaleArray.map((_, i) => i === targetIndex ? MAX_SCALE : MIN_SCALE);
 }

 build() {
 Column() {
 Swiper(this.ctrl) {
 ForEach(this.colorArray, (color: Color, index: number) => {
 Column()
 .width('100%')
 .height('100%')
 .backgroundColor(color)
 .scale({ x: this.scaleArray[index], y: this.scaleArray[index] })
 .animation({ duration: PAGE_DURATION, curve: Curve.Linear })
 .borderRadius(12)
 }, (color: Color, index: number) => `card_${index}`);
 }
 .displayMode(SwiperDisplayMode.STRETCH)
 .displayCount(1)
 .width('100%')
 .height('100%')
 .index(this.currentIndex)
 .cachedCount(1)
 .indicator(true)
 .duration(SWIPER_DURATION)
 .itemSpace(0)
 .prevMargin(20)
 .nextMargin(20)
 .curve(Curve.Linear)
 .onGestureSwipe((i, e) => this.onGestureSwipe(i, e))
 .onAnimationStart((i, t) => this.onAnimationStart(i, t))
 .onChange(i => this.currentIndex = i)
 .onAnimationEnd(() => this.startSwiperOffset = 0)
 }
 .width('100%')
 .height('100%')
 .justifyContent(FlexAlign.Center)
 }
}

Test Results

  • Verified that the middle card scales to MAX while neighbors scale to MIN, with smooth interpolation during drag and settle.
  • With displayMode=STRETCH and tuned prevMargin/nextMargin, partial neighbors remain visible.

👁 Q777.gif

Limitations or Considerations

  • Choose DRAGGING_MAX_DISTANCE, prevMargin/nextMargin, and cachedCount based on screen width & memory.
  • If content is heavy (e.g., large images), keep item views light to prevent jank during per-frame callbacks.
  • For wearables (round screens), reduce margins and scale delta for better legibility and avoid edge clipping.
  • indicator(true) can visually collide with large margins; hide or reposition if needed.

Related Documents or Links

Written by Bunyamin Eymen Alagoz