ANTIGRAVITY LABEN
記事一覧/アプリ開発
アプリ開発/2026-06-22上級

壁紙ビューアのスライドショーとページ送りスクラバーを iOS から Android へ逆移植する — SnapHelper との双方向同期でつまずいた点

iOS 版の全画面壁紙ビューアにあったスライドショーと下部スクラバーを Android へ逆移植した実装記録。RecyclerView と SnapHelper で現在ページを確定させ、スクラバーと双方向同期し、自動送りとユーザー操作の競合を状態機械で解いた手順を Kotlin の動くコードで共有します。

android21kotlin6recyclerview2ios29indie-dev6

プレミアム記事

個人開発で壁紙アプリを iOS と Android の両方で出していると、片方で好評だった機能をもう片方にも揃えたくなります。今回は逆方向でした。iOS 版の全画面ビューアに先に入れていた「スライドショーで自動送りしつつ、画面下部のスクラバーを掴めば任意のページへ一気に飛べる」操作を、Android 版へ持っていく作業です。

逆移植そのものは Antigravity に任せました。iOS の UICollectionView 実装を読ませて「これと同じ挙動を RecyclerView で」と頼むと、土台はすぐ組み上がります。ところが、実機で触ると現在ページの表示が一拍ずれる。スクラバーを動かすとビューアが二重にスクロールする。スライドショー中に指で送ると画面がガタつく。素直に動かないところに、iOS と Android の設計思想の違いがそのまま現れていました。

詰まった三点を、順番に解いていった記録です。

まず「現在ページが確定する瞬間」が iOS と Android で違う

最初の違和感は、スクラバーに表示するページ番号が実際の表示より遅れて追従することでした。原因は、ページ位置を確定させるタイミングのモデルが両プラットフォームで根本的に異なるからです。

iOS の UICollectionViewisPagingEnabled = true にすると、1 画面ぴったりで止まり、scrollViewDidEndDecelerating が呼ばれた時点でページが確定します。フレームワークが「1 ページ = 1 画面幅」を保証してくれるので、contentOffset.x / pageWidth を四捨五入すれば現在ページが取れます。

Android の RecyclerView には、この「ページング」の概念が組み込まれていません。スナップは SnapHelper という別部品が後付けで担当します。さらにスクロール状態の遷移が iOS と異なり、指を離した後に SETTLING(慣性で滑っている)を経て IDLE に落ちます。SETTLING の最中にページを読むと、まだ滑走中の中途半端な位置を拾ってしまう。これが「一拍ずれる」の正体でした。

観点iOS(UICollectionView)Android(RecyclerView + SnapHelper)
ページングの担い手フレームワーク標準(isPagingEnabled)後付けの SnapHelper
現在ページの確定契機scrollViewDidEndDeceleratingスクロール状態が IDLE に落ちた瞬間
位置の読み方contentOffset から算出snapHelper.findSnapView() で中央ビューを取得
滑走中の中間状態意識せずに済むSETTLING を明示的に除外する必要がある

ここを理解しないまま addOnScrollListener の中で毎フレーム位置を読むと、スクラバーが小刻みに震えます。「確定は IDLE のときだけ」という線引きが出発点になります。

RecyclerView + PagerSnapHelper で 1 枚ずつ止める土台

PagerSnapHelper を取り付ける

逆移植の土台は、横スクロールの RecyclerViewPagerSnapHelper を取り付けるところから始まります。PagerSnapHelper は 1 回のフリングで 1 ページだけ進む、まさに iOS の paging に相当する挙動を作ってくれます。

class WallpaperViewer(
    private val recyclerView: RecyclerView,
    private val scrubber: SeekBar,
    private val itemCount: Int,
) {
    private val snapHelper = PagerSnapHelper()
 
    init {
        recyclerView.layoutManager =
            LinearLayoutManager(recyclerView.context, RecyclerView.HORIZONTAL, false)
        // 1 フリング = 1 ページ。複数ページ飛ばさせない
        snapHelper.attachToRecyclerView(recyclerView)
        scrubber.max = (itemCount - 1).coerceAtLeast(0)
    }
 
    /** SnapHelper が中央に吸着しているビューのアダプタ位置を返す。なければ -1。 */
    private fun currentPage(): Int {
        val lm = recyclerView.layoutManager ?: return -1
        val view = snapHelper.findSnapView(lm) ?: return -1
        return lm.getPosition(view)
    }
}

findFirstVisibleItemPosition では位置がずれる

findSnapView() が返すのは「いま中央にスナップしているビュー」です。LinearLayoutManager.findFirstVisibleItemPosition() を使う実装例をよく見かけますが、スナップ位置とは一致しないことがあり、スクラバーがずれる原因になります。私自身、最初はそちらで書いて 1 ページずれる不具合を踏みました。スナップ位置はあくまで snapHelper.findSnapView() から取るのが正解です。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
iOS の UICollectionView paging をそのまま Android に持ち込んで「現在ページがずれる」問題で詰まっていた人が、SnapHelper を使った正しい確定タイミングで解決できる
ビューアとスクラバー(SeekBar)を双方向同期させるときに必ず起きる無限ループを、発生源フラグで断ち切る実装パターンを習得できる
スライドショーの自動送りとユーザーのスワイプが衝突して画面がガタつく問題を、3 状態の状態機械で整理し、自分のギャラリー系画面に応用できる
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

アプリ開発2026-05-17
課金状態の「正」はどこにある? — Antigravityで設計するad-free Source of Truthパターン
累計5,000万DLアプリ開発で実証した課金状態管理の設計パターン。BillingManager・AdFreeManager・ModalGateの3層設計をAntigravityで実装する実践ガイド。
アプリ開発2026-04-05
Antigravity × Compose Multiplatform 実装ガイド:UI共有からDesktop対応まで、統合クロスプラットフォーム開発2026
Compose Multiplatform(CMP)と Antigravity AI IDE を組み合わせ、iOS・Android・Desktop のUI層を共有する高品質クロスプラットフォームアプリを構築する実践ガイド。設計パターン・Antigravity活用・自動テスト・リリースフローまでを網羅。
アプリ開発2026-06-17
ダイアログが重なる前に止める — 課金・レビュー誘導・リワード広告を1か所のゲートで束ねる
ペイウォール・レビュー誘導・リワード広告が同じ瞬間に重なって出る不具合を、優先度つきの中央ゲートで根治した実装記録です。Antigravityのエージェントに散らばったshow()呼び出しの掃き出しを任せ、表示ポリシーは自分で握る線引きで進めました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →