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
-
Swiper: container for paged horizontal/vertical scrolling.
-
prevMargin: show a sliver of the previous page. -
nextMargin: show a sliver of the next page. -
onGestureSwipe: per-frame callback while the user drags. -
onAnimationStart→onChange→onAnimationEnd: animation lifecycle. -
customContentTransition: per-frame callback (during drag & settling) to setopacity/scale/translate/zIndex.
Docs:
-
Event order: onGestureSwipe → onAnimationStart → onChange → onAnimationEnd.
Implementation Steps
Approach A — Event Combo (gesture + animation lifecycle)
- Set
prevMargin/nextMarginso neighbors are partially visible. - Track drag distance in
onGestureSwipeand compute current/prev/next scales. - In
onAnimationStart, snap scales to MAX for target and MIN for neighbors. - Update
currentIndexinonChange. - Reset helpers in
onAnimationEnd.
Approach B — customContentTransition (single place)
- Provide a data source and initial
scaleArray. - In
onChange, mark the selected page as MAX and neighbors as MIN. - In
customContentTransition.transition(proxy), compute per-frame current/next/prev scale fromproxy.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=STRETCHand tunedprevMargin/nextMargin, partial neighbors remain visible.
Limitations or Considerations
- Choose
DRAGGING_MAX_DISTANCE,prevMargin/nextMargin, andcachedCountbased 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
- Swiper
- prevMargin
- nextMargin
- onGestureSwipe
- onAnimationStart
- onChange
- onAnimationEnd
- customContentTransition

Top comments (0)