◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/アプリ開発
◇ アプリ開発/2026-05-28中級

壁紙アプリにスライドショーとページジャンプを実装した手順と判断

綺麗な壁紙 4K/HDR と浮世絵壁紙の Android 版に追加したスライドショーとページジャンプの実装メモです。Handler とライフサイクル、SeekBar 連携、広告との兼ね合いで悩んだ判断を残します。

Android55RecyclerView3ViewPager2個人開発218壁紙アプリ28

ユーザーレビューに何年も繰り返し届いていた要望が 2 つありました。「ベッドで眺めるときに自動でめくれるようにしてほしい」と「壁紙の枚数が多すぎて目当ての一枚に辿り着けない」です。今回 Android 版の「綺麗な壁紙 4K/HDR」と「浮世絵壁紙」をアップデートし、この 2 点に応えるかたちで スライドショー と ページジャンプスライダー を追加しました。あわせて、これも長く要望をいただいていた 広告非表示オプション も同時にリリースしています。実装そのものは特別なことをしていませんが、2014 年から壁紙アプリを運営してきた個人開発の感覚で、迷った場面と選んだ理由を残しておきます。

既存の RecyclerView を壊さずに自動送りを差し込む

スライドショーは「3 秒ごとに次の壁紙を表示する」だけの機能ですが、自前で Thread を回すと画面回転やバックグラウンド遷移でリークしやすくなります。今回は ViewPager2 が背後に持つ RecyclerView をそのまま動かす方針にして、UI スレッドの Handler を使う形に落としました。

private val mainHandler = Handler(Looper.getMainLooper())
private val advanceRunnable = object : Runnable {
    override fun run() {
        val next = viewPager.currentItem + 1
        if (next < adapter.itemCount) {
            viewPager.setCurrentItem(next, true)  // smoothScroll = true
        } else {
            viewPager.setCurrentItem(0, true)     // 末尾で先頭へループ
        }
        mainHandler.postDelayed(this, INTERVAL_MS)
    }
}
 
fun startSlideshow() {
    mainHandler.removeCallbacks(advanceRunnable)
    mainHandler.postDelayed(advanceRunnable, INTERVAL_MS)
}
 
fun stopSlideshow() {
    mainHandler.removeCallbacks(advanceRunnable)
}

ポイントは「画面タップで停止」を onTouchEvent ではなく ViewPager2.OnPageChangeCallback#onPageScrollStateChanged 経由で拾うことです。ユーザーが手でスワイプしたときも自動送りと判定が衝突しないように、SCROLL_STATE_DRAGGING を検知したら stopSlideshow() を呼ぶ作りにしています。タップ判定だけで止めようとすると、慣性スクロール中に次のページへ進んでしまう不具合が初期実装で出ました。

ページジャンプは SeekBar とプレビューの 2 層構造

10 万枚規模の中から特定の一枚に飛ぶ機能を、シークバー 1 本で済ませました。素朴な作りですが、OnSeekBarChangeListener の中で viewPager.setCurrentItem(progress, false) を直接呼ぶと、ドラッグ中に毎フレームレイアウトが走ってカクつきます。

seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
    override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
        if (!fromUser) return
        previewLabel.text = "${progress + 1} / ${adapter.itemCount}"  // 軽量プレビュー
    }
    override fun onStartTrackingTouch(sb: SeekBar?) {
        stopSlideshow()  // ジャンプ中は自動送りを止める
    }
    override fun onStopTrackingTouch(sb: SeekBar?) {
        viewPager.setCurrentItem(sb?.progress ?: 0, false)  // 確定時のみ実位置を動かす
    }
})

ドラッグ中はテキストで「現在地 / 総数」を出すだけに留め、指を離したタイミングで初めて setCurrentItem を呼びます。10 万件規模でもスクラブが滑らかになり、Glide 側のキャンセルと再ロードも最後の 1 回で済むため、メモリ圧と通信量の両方に効きました。

広告非表示オプションとの兼ね合い

「広告を非表示にする選択肢が欲しい」という声も多くいただいていたので、今回のリリースで合わせて入れています。動作の実体は、永久課金 / サブスクリプションによる ad-free と、リワード広告視聴による期間限定 ad-free の状態を OR で合成した値を Globals.SHOW_AD として参照する仕組みです。スライドショー中はインタースティシャル広告が割り込むと体験が壊れるため、自動送りが走っている間は背面のインタースティシャル呼び出しを抑制する条件分岐を入れました。

過去にバックボタン経由の広告ゲートを入れ子で書いていた頃、優先度が暗黙化してデバッグが本当に辛くなった記憶があります。その反省を継いで、今回はスライドショーフラグ・ad-free フラグ・広告カウンタを独立した変数として並列に扱うようにしました。状態を並べておくと、後から「誰が誰を抑制したか」をログだけで追えるようになります。

ロック・回転・バックグラウンドへの対応

onPause で必ず stopSlideshow() を呼ぶ、onResume では自動で再開しないという地味な対応が重要でした。スライドショーが裏で回ったまま画面ロックされると、解除した瞬間に複数枚スキップして見えてしまうためです。

回転時は ViewModel に「現在ページ」と「スライドショー実行中フラグ」を保存し、onSaveInstanceState でも併用しています。両方持つのは冗長ですが、累計 5,000 万 DL のレンジで動かすと「Activity 全破棄を伴うクラッシュ復帰」と「単純な構成変更」のどちらも一定頻度で出るので、保険として残しています。

次に検討していること

スライドショーの間隔を設定画面から変えられるようにすること、ロック画面に直接設定するショートカットを足すこと、そして同じ仕組みを iOS 版に逆移植することを次の課題に置いています。祖父が宮大工で「手を動かすことが一つの信心」と教わって育った家庭の感覚が私にも残っていて、初回リリース時の設計に対して納得していない部分が今もあります。今回のアップデートはその一部を少しずつ整理していくきっかけになりました。

もしご利用のなかでお気づきの点があれば、Google Play のレビューから教えていただけると嬉しいです。実機でしか拾えない挙動が必ずあるので、引き続き 1 件ずつ手で返信しながら反映していきます。お読みいただきありがとうございました。

シェア

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

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

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

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

◇ アプリ開発2026-05-29
Android 壁紙アプリ v2.1.0 公開後の保守 — Crashlytics と Claude in Chrome で回す日々の運用
Beautiful 4K/HDR Wallpapers v2.1.0 と Ukiyo-e Wallpapers v1.8.0 の段階公開後、Crashlytics 監視と新規クラッシュ対応を Claude in Chrome に任せる運用に切り替えました。RecyclerView の防御的コピーや Glide の desugar など、実機で踏んだ落とし穴を保守目線でまとめます。
◇ アプリ開発2026-09-07
アプリから外部の AI にユーザーの文章を送るとき、Play の「データの安全性」に何を書くか
外部の AI にユーザーの入力を渡すアプリで、Play のデータ安全性フォームの収集・共有・一時処理をどう区分するか。送信を一箇所に集める実装と、順序を誤ると消し漏れるマスキングの実測を添えて整理します。
◇ アプリ開発2026-10-15
EAS Build だけ「No matching variant」で落ちるとき、まず疑う手元の android/build
手元の Gradle は通るのに、EAS Build の Android だけが「No matching variant of project」で止まる。GitHub の報告を読み比べ、Windows で生成した autolinking のキャッシュが Linux のビルド機へ渡っていた経路と、ビルドを投げる前に確かめる診断スクリプトをまとめました。