feat(screensaver): add real-time interactive photo drag & direction-aware slide transition - #166
Conversation
starbrightlab
left a comment
There was a problem hiding this comment.
Thanks for this — the gesture/animation work is genuinely nicely built (the slide-completion math resuming from the live drag translationX and the snap-back reset are clean). But there are two blocking issues that would bite on real Portal hardware, so requesting changes.
Blockers
1. Main-thread I/O in the touch handler → crash. setupAdjacentDragBitmap() is called from onTouch ACTION_MOVE (main thread), and on a cache miss it falls back to a synchronous decode/fetch (PhotoFrameController.kt:246,251). For remote sources that's fetchRemoteImage on the UI thread → NetworkOnMainThreadException → launcher crash (and a crash on a home app clears the HOME preferred-activity association). It fires on the first swipe of any neighbor whose preload hasn't landed. The fallback must run on the io executor with the layer wired up in the posted ui continuation — never inline in ACTION_MOVE.
2. LruCache(8) is entry-count sized → OOM. LruCache<String,Bitmap>(8) (PhotoFrameController.kt:214) has no sizeOf override, so it's 8 entries regardless of pixel size. A 12MP original at inSampleSize=1 ≈ 48 MB ARGB_8888 × 8 ≈ ~300 MB retained, on top of the live layers + blur copies. That's a guaranteed OOM/heavy-GC regression on the RAM-limited Portal. Size the cache in bytes as a fraction of Runtime.maxMemory() (override sizeOf → bitmap.byteCount) and cap to prev/current/next (2–3 entries).
Should-fix
- Dwell timer races the drag —
ACTION_DOWN/MOVEnever cancel the auto-advance tick, so it can flipactiveLayerIndex/ resettranslationXmid-drag and move the wrong layer. Suspend the tick onACTION_DOWN/while dragging, reschedule on UP/snap-back. ACTION_CANCELcan trigger dismiss — UP and CANCEL share a branch (:300); a CANCEL with small dx/dy falls through to the tap test and callsonExit. GateonExittoACTION_UPonly.- Black gap on incomplete preload — if the incoming bitmap is null, the code still sets
incomingLayertranslation and the UP slide animates a blank/stale layer in. Guard the commit unless a real incoming bitmap was set.
Nits: no multi-touch/pointer-id handling; the index math + slide-duration calc are pure and worth a small unit test.
Happy to help once these are addressed — the core UX is worth landing.
…-tick race, and ACTION_CANCEL exit
…es and DecelerateInterpolator(1.8f) curve
d664ec0 to
72e04ad
Compare
|
PR #166 has been rebased on top of current main (v1.67)! All requested review items are included:
|
starbrightlab
left a comment
There was a problem hiding this comment.
I test-merged this against current main (it merges cleanly even after #202 touched the same file) and it builds with 333 tests green. The gesture work reads well — the 1:1 drag, the release momentum, and keeping the auto-dwell crossfade separate from the interactive slide are all nicely done. One thing needs fixing before it goes in.
The preload cache has a floor, not a cap
// Sized by memory byte count (max 1/8th of available max heap, capped between 16MB and 32MB) to prevent OOM.
val maxMemoryKb = (Runtime.getRuntime().maxMemory() / 1024).toInt()
val cacheSizeKb = (maxMemoryKb / 8).coerceIn(16 * 1024, 32 * 1024)coerceIn(min, max) raises the value when it's below the minimum, so this can never be smaller than 16MB — the opposite of what the comment describes on exactly the devices that need it smallest. If 1/8 of the heap is 8MB, this asks for 16MB anyway.
That matters here because the app doesn't request largeHeap, so on Android 9/10 Portals the per-app heap can be modest, and the screensaver is the thing left running unattended for hours. 16MB of decoded bitmaps is on top of the two full-screen ImageView layers the frame already holds plus the blur copies — and #202 just landed a setting that keeps a blurred bitmap around in fit mode.
The fix is a one-liner — cap rather than floor:
val cacheSizeKb = (maxMemoryKb / 8).coerceAtMost(32 * 1024)with a small floor only if you want one (coerceIn(4 * 1024, 32 * 1024)), which would then match the comment.
Portal Go, where you verified this, is one of the roomier models — worth a run on a Gen-1 Portal or Mini after the change, ideally with a large local folder so the cache actually fills.
Minor
.gitignore gains referencias_privado, which looks like a personal working directory rather than something the project needs. Better in your global gitignore or .git/info/exclude than in the repo's.
Happy to re-check once the sizing is fixed — everything else here looked sound to me, including velocityTracker being recycled on all three exit paths.
Generated by Claude Code
…e out of gitignore
|
Thanks for the detailed review — both fixed and pushed.
I pushed on top of the existing base rather than rebasing, so the clean test-merge you did still holds. Unit tests green locally. One thing worth your call on the floor: a single full-screen ARGB_8888 bitmap on a 1280×800 Portal is ~4.1 MB, so a fixed 4 MB floor sits just under one entry. If On device testing: I only have a Portal Go, so I can't verify Gen-1 or Mini myself. If it's useful I can push a one-line debug log of |
Summary of Changes
This PR introduces real-time interactive touch gesture navigation and direction-aware slide transitions to the screensaver (
PhotoFrameController):1. Interactive 1:1 Touch Drag Navigation (
ACTION_MOVE)DecelerateInterpolator, or snaps back cleanly if released under threshold.2. Background Adjacent Photo Preloading (
LruCache)LruCache<String, Bitmap>for preloading adjacent (next & previous) photos on a background thread pool immediately after any photo displays.3. Preserved Auto-Dwell Transition
Verification & Unit Tests
./gradlew :app:testDebugUnitTest(BUILD SUCCESSFUL)../gradlew :app:assembleDebug).