Two requests had been turning up in user reviews for years. One was "let me lie in bed and have the wallpapers advance on their own," and the other was "there are too many wallpapers, I cannot reach the one I want." This week I shipped an update for the Android editions of Beautiful 4K/HDR Wallpapers and Ukiyo-e Wallpapers that adds a slideshow and a page-jump slider to address both. The same release also introduces an ad-free option, which is another long-standing request. The code itself is not exotic, but as an indie developer who has been running wallpaper apps since 2014, I want to leave a record of the moments where I hesitated and what I chose.
Threading auto-advance through the existing RecyclerView
A slideshow that "moves to the next wallpaper every three seconds" sounds trivial, but rolling your own Thread tends to leak across rotations and background transitions. I decided to keep the underlying RecyclerView that ViewPager2 drives, and orchestrate everything from a UI-thread 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) // loop back to the start
}
mainHandler.postDelayed(this, INTERVAL_MS)
}
}
fun startSlideshow() {
mainHandler.removeCallbacks(advanceRunnable)
mainHandler.postDelayed(advanceRunnable, INTERVAL_MS)
}
fun stopSlideshow() {
mainHandler.removeCallbacks(advanceRunnable)
}The detail that mattered was catching "tap to stop" through ViewPager2.OnPageChangeCallback#onPageScrollStateChanged rather than onTouchEvent. When the user swipes manually, I want auto-advance and manual scroll never to fight each other, so I call stopSlideshow() the moment SCROLL_STATE_DRAGGING is observed. My first attempt only listened for taps, and it produced a bug where inertial scrolling kept advancing pages after the user had clearly tried to stop the show.
Page jump as a two-layer SeekBar with a lightweight preview
I wanted users to reach a specific image out of a hundred thousand with a single slider. The naive approach—calling viewPager.setCurrentItem(progress, false) straight from OnSeekBarChangeListener—runs layout every frame during the drag and stutters badly.
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(sb: SeekBar?, progress: Int, fromUser: Boolean) {
if (!fromUser) return
previewLabel.text = "${progress + 1} / ${adapter.itemCount}" // lightweight preview
}
override fun onStartTrackingTouch(sb: SeekBar?) {
stopSlideshow() // pause auto-advance while the user is scrubbing
}
override fun onStopTrackingTouch(sb: SeekBar?) {
viewPager.setCurrentItem(sb?.progress ?: 0, false) // commit only on release
}
})While the user drags, I only update a text label that reads "current / total." The real setCurrentItem call happens once, the moment the finger lifts. Even at a hundred-thousand-item scale the scrub stays smooth, and Glide's cancellation and reload only fire once at the final destination, which is friendlier to both memory and bandwidth.
How this interacts with the ad-free option
The "let me hide the ads" request has come in for years, so I bundled it into this release. Internally, the ad-free state is the OR of two independent sources of truth: permanent purchase or subscription on one side, and a time-bounded ad-free state earned by watching a rewarded ad on the other. The combined value is exposed as Globals.SHOW_AD. During an active slideshow, an interstitial would shatter the experience, so I added a guard that suppresses background interstitial calls while auto-advance is running.
A while back I had ad gating logic nested inside other gating logic, and the implicit priority order made debugging genuinely painful. Learning from that, I now keep the slideshow flag, ad-free flag, and ad counters as parallel, independent variables. When the state machine is flat, you can later trace "who suppressed whom" purely from logs.
Lock, rotation, and background transitions
The unglamorous fix that mattered most was: always call stopSlideshow() in onPause, and never auto-resume in onResume. If the slideshow keeps running while the device is locked, the user unlocks to find that several images have silently flown past.
For rotation, I save both the current page index and the "slideshow is running" flag in a ViewModel, and double up with onSaveInstanceState. Carrying both feels redundant, but at the cumulative download scale my wallpaper apps operate at—north of fifty million across the family—you see both clean configuration changes and full-Activity crash recoveries with non-trivial frequency, so the belt-and-suspenders setup earns its keep.
What I am still working on
The next items on my list are letting users adjust the slideshow interval from the Settings screen, adding a shortcut that sets the wallpaper directly from the lock screen, and back-porting the same machinery to the iOS edition. I grew up around grandfathers who were temple carpenters and treated "moving your hands" as a form of devotion, and that has stayed with me—I am still not at peace with some of the original release decisions in these apps. This update is one of the steps I am taking, slowly, to tidy them up.
If anything feels off while you use it, please leave a note in the Google Play review and I will reply by hand. There are behaviors only real devices reveal, and I plan to keep responding one review at a time. Thank you for reading.