惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 叶小钗
Microsoft Azure Blog
Microsoft Azure Blog
Stack Overflow Blog
Stack Overflow Blog
Jina AI
Jina AI
Vercel News
Vercel News
H
Help Net Security
Martin Fowler
Martin Fowler
美团技术团队
云风的 BLOG
云风的 BLOG
Y
Y Combinator Blog
阮一峰的网络日志
阮一峰的网络日志
MyScale Blog
MyScale Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
博客园 - 司徒正美
人人都是产品经理
人人都是产品经理
Engineering at Meta
Engineering at Meta
G
Google Developers Blog
Blog — PlanetScale
Blog — PlanetScale
MongoDB | Blog
MongoDB | Blog
宝玉的分享
宝玉的分享
小众软件
小众软件
T
Tailwind CSS Blog
WordPress大学
WordPress大学

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Visible-Edge Card Carousel with Swiper (ArkUI) — prevMarg...
HarmonyOS · 2026-04-23 · via DEV Community

HarmonyOS

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)
  }
}

Enter fullscreen mode Exit fullscreen mode

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