開發者剛開源了「ComposePinchGrid」——一個為 Compose Multiplatform 打造的 Goo
開發者剛開源了「ComposePinchGrid」——一個為 Compose Multiplatform 打造的 Google Photos 風格捏縮網格元件。可在 Android、iOS、Desktop 和 Web 上運作,只需 3 行程式碼,且零 Material 依賴。
手勢辨識難點 真正的難點不在網格本身,而在手勢辨識。transformable() 與 LazyVerticalGrid 捲軸的衝突在於兩者搶奪相同的指標事件。解決方案是使用原始的 pointerInput 搭配 PointerEventPass.Initial——在網格捲軸處理器之前截獲 2 指捏縮手勢,單指捲軸則完全通過。
元件功能清單 元件提供的功能包括:
- 非對稱閾值調整(捏展自然產生較弱的縮放,透過補償實現平衡)
- 死區過濾(消除手指顫抖造成的抖動)
- 呼吸動畫效果(使用 graphicsLayer 達到零重組)
- 平台原生震動回饋(Android 使用 CLOCK_TICK,iOS 使用 UISelectionFeedbackGenerator)
跨平台架構 架構採用 Kotlin Multiplatform 開發,支援 Android、iOS、Desktop 和 Wasm,採用 Apache 2.0 授權。
原生手勢選擇 技術特點在於為何選擇原始 pointerInput 而非 transformable——transformable 乍看是捏縮手勢的顯然選擇,卻與 LazyVerticalGrid 捲軸衝突。兩者競爭相同的指標事件,單指捲軸會被誤判為變換開始,導致網格凍結而非捲動。ComposePinchGrid 使用 awaitEachGesture 搭配 awaitFirstDown,只在偵測到 2 個或更多指標時才消費事件,單指捲軸完全不受影響。
閾值參數調整 使用者可透過 thresholdFraction 參數控制觸發欄數變更需要的捏縮程度(較低數值代表更靈敏),pinchOutThresholdMultiplier 在 0.85f 時讓放大需要 15% 較少的手指移動,使兩個方向感覺同樣反應靈敏。deadZone 預設 0.01f 過濾微小動作,防止手指顫抖造成的抖動。
呼吸動畫設計 呼吸動畫在捏縮手勢期間讓網格隨指標縮放,提供即時視覺回饋,使用 graphicsLayer 實現零重組的 GPU 變換。breathingScaleIntensity 預設 0.10f(±10% 縮放),可設為 0f 禁用;breathingReturnDuration 預設 150ms 控制釋放時回歸動畫速度。
捲軸位置保留 欄數變更時,網格會快照首個可見項目索引並在變更後復原,防止天真的 GridCells.Fixed() 交換帶來的卡頓捲軸跳躍。最佳實踐是提供穩定的 key 值。
程式控制支援 手勢感應可完全禁用,保留程式控制能力,透過 snapToColumn() 方法實現鍵盤、按鈕或無障礙功能的欄數控制。performHapticFeedback 在每次欄數變更時自動觸發,可透過 hapticEnabled = false 禁用。
效能考量 效能考量上:
- 呼吸動畫使用 graphicsLayer 純繪製階段零重組
- 欄數變更單次 mutableIntStateOf 更新觸發網格重組
- 震動回饋內聯執行無協程開銷
- 捲軸復原採標準 Compose 模式的 LaunchedEffect + snapshotFlow
高度可配置性 元件的可配置性極高——每個參數都有經過調整的預設值,但可覆蓋任何設定。thresholdFraction 預設 0.45f 提供反應靈敏但不易意外觸發的平衡;pinchOutThresholdMultiplier 預設 0.85f 補償捏展自然較弱的特性;breathingScaleIntensity 預設 0.10f 提供適度視覺回饋;transitionSpec 預設即時重排,也支援 crossfade 等過渡效果。
I just open-sourced ComposePinchGrid — a Google Photos-style pinch-to-resize grid for Compose Multiplatform.
— Adit Lal (@aditlal) March 17, 2026
Pinch to change columns, haptic on snap, breathing scale animation. Works on Android, iOS, Desktop & Web.
3 lines of code. Zero Material dependency.… pic.twitter.com/VxjDhL7bFZ
The hard part wasn't the grid - it was the gesture.
— Adit Lal (@aditlal) March 17, 2026
transformable() fights with LazyVerticalGrid scroll. Both grab the same pointer events.
Solution: raw pointerInput with PointerEventPass.Initial — intercepts 2-finger pinch BEFORE the grid's scroll handler. Single-finger…
What you get:
— Adit Lal (@aditlal) March 17, 2026
- Asymmetric thresholds (pinch-out is naturally weaker - we compensate)
- Dead zone filtering (no jitter from finger tremors)
- Breathing scale animation (graphicsLayer = zero recompositions)
- Platform haptics (Android CLOCK_TICK, iOS UISelectionFeedbackGenerator)…
Docs: https://t.co/qKpVqH2F63
— Adit Lal (@aditlal) March 17, 2026
GitHub: https://t.co/1EEkeUDVt0
KMP from day one. Android, iOS, Desktop, Wasm. Apache 2.0.
