Handling Autoplay in Scrollable Containers
When FwPlayerDeckView is placed inside a scrollable parent (e.g., NestedScrollView, Compose Column with verticalScroll, or LazyColumn), it cannot automatically detect that the outer container is scrolling. This page explains the problem, provides two approaches to solve it, and documents known limitations in Jetpack Compose.
Table of Contents
Background
FwPlayerDeckView has built-in lifecycle callbacks that handle window-level visibility:
onAttachedToWindow()/onDetachedFromWindow()— fires when the view is added to or removed from the window.onWindowFocusChanged()— fires when the hosting window gains or loses focus.
These callbacks work correctly for basic scenarios (e.g., the Activity goes to the background). However, they do not fire when a parent scrollable container clips the view out of the visible area. When the user scrolls the FwPlayerDeckView off-screen inside a NestedScrollView or Compose scrollable Column, the view remains attached to the window and the window retains focus. As a result, autoplay continues even though the PlayerDeck is no longer visible to the user.
We provide two approaches to solve this problem.
Approach 1: Manual Viewport Notification
The host code detects scrolling itself and calls onViewPortEntered() / onViewPortLeft() on the FwPlayerDeckView when it crosses a visibility threshold (e.g., 50%).
Pros:
Full control over visibility detection logic.
Works reliably in all environments (traditional Views and Compose).
Compatible with any scrollable container, including
LazyColumn.
Cons:
Requires manual scroll tracking and visibility calculation.
More boilerplate code in the host.
Demo 1A: Traditional View (Fragment + NestedScrollView)
Layout XML (fragment_player_deck_scrollable.xml):
Fragment (PlayerDeckScrollableFragment.kt):
How it works:
An
OnScrollChangedListeneris registered on theNestedScrollView'sViewTreeObserver.On each scroll event (throttled to every 100ms),
getGlobalVisibleRect()computes how much of theFwPlayerDeckViewis on screen.When the visible ratio drops below 50%,
onViewPortLeft()pauses playback.When the visible ratio reaches 50% or above,
onViewPortEntered()resumes playback.
Demo 1B: Compose (Activity + Scrollable Column)
This example shows manual viewport notification in a Compose-based UI, using rememberScrollState() and the update block of AndroidView to track visibility.
How it works:
rememberScrollState()tracks the scroll position of theColumn. ReadingscrollState.valuein the composable body triggers recomposition whenever the scroll position changes.Critical:
currentScrollValueis referenced inside theupdatelambda. This is required because Compose's strong-skipping mode (enabled by default since Compose compiler 1.5.4+) remembers lambdas based on their captured values. Without this reference, the lambda only captureslastVisibilityState(aMutableStateobject whose reference never changes), so Compose considers the lambda unchanged and skips callingupdateentirely — even though the parent composable recomposed.In the
updateblock,getGlobalVisibleRect()computes the visible portion of theAndroidViewon screen (consistent with Demo 1A and the SDK's internal tracker).The
lastVisibilityStateguard prevents redundantonViewPortEntered()/onViewPortLeft()calls — only actual state transitions trigger notification.onViewPortEntered()/onViewPortLeft()are called when the visible ratio crosses the 50% threshold.
Approach 2: Automatic Visibility Tracking
Call setVisibilityTrackingEnabled(true) before or after init(). The SDK will internally track visibility and pause/resume playback automatically. No manual scroll-tracking code is needed.
Pros:
Zero manual scroll tracking code required.
Single line of configuration.
Cons:
May not work in certain Compose scenarios (see Known Limitations below).
Demo 2A: Traditional View (Fragment + NestedScrollView)
Use the same XML layout as Demo 1A. The Fragment is much simpler because no scroll listener is needed:
Compared to Demo 1A, the only addition is:
No OnScrollChangedListener, no visibleRatio() calculation, no manual onViewPortEntered()/onViewPortLeft() calls.
Demo 2B: Compose (Activity + Scrollable Column)
Key point: setVisibilityTrackingEnabled(true) can be called before or after init(). In the example above it is called before init() inside the factory block so the tracker starts as soon as the view is initialized.
Important: Use a regular scrollable
Column(withModifier.verticalScroll()), notLazyColumn. See the limitations section below.
How Automatic Tracking Works Internally
When setVisibilityTrackingEnabled(true) is called, the SDK creates a ViewVisibilityTracker that monitors the view using the Android View system:
Listeners registered:
OnGlobalLayoutListenerandOnScrollChangedListeneron the view'sViewTreeObserver. AnOnAttachStateChangeListenerhandles window attach/detach events.Visibility computation: On each scroll or layout event (throttled to every 50ms to avoid excessive computation), the tracker calls
getGlobalVisibleRect()on the view and computes:Threshold: If
visibleFraction >= 0.5(50%), the view is considered visible. If it drops below 50%, the view is considered hidden.Callback: When the visibility state changes, the tracker notifies the
FwPlayerDeckView, which callsplayerManager.pauseTemporarily()orplayerManager.resumeFromTemporaryPause()accordingly.Cleanup: The tracker is automatically stopped when
destroy()is called.
Known Limitations of Automatic Tracking in Compose
The automatic visibility tracking (setVisibilityTrackingEnabled) relies on Android View system mechanisms (ViewTreeObserver, getGlobalVisibleRect). In certain Jetpack Compose scenarios, these mechanisms do not work correctly.
1. LazyColumn / LazyRow / LazyVerticalGrid
LazyColumn and similar lazy composables dispose the AndroidView entirely when items scroll out of the visible area. The view is destroyed (removed from the composition tree) rather than merely scrolled off-screen.
Impact:
The
ViewVisibilityTrackernever gets a chance to detect "scrolled out" — the view is simply gone.Autoplay does stop (because the view is detached from the window), but re-entering the viewport creates a brand new view instance, losing all internal state.
The
onReleasecallback fires on every scroll-out, callingdestroy(), andfactoryfires on every scroll-in, callinginit()again.
2. Deeply Nested Compose Layout Nodes with Clipping
If the AndroidView is nested inside multiple Compose containers that apply Modifier.clip() or custom clipping, getGlobalVisibleRect() may not accurately reflect Compose-level clipping.
Impact:
The Android View system and Compose layout system track clipping independently.
The visible rect may report the view as "visible" when Compose has actually clipped it, causing autoplay to continue when the view is not visible to the user.
3. Compose Scroll Containers That Do Not Trigger OnScrollChangedListener
While Modifier.verticalScroll() typically propagates scroll events through the underlying AndroidComposeView, there are edge cases where offset changes are handled purely in Compose's layout system and never dispatched as traditional scroll events to ViewTreeObserver:
Modifier.offsetwith animated values — the offset is applied at the Compose layout level.Custom
NestedScrollConnection— scroll deltas may be consumed before reaching the View layer.HorizontalPager/VerticalPager— page transitions use Compose-internal animation.
Impact:
The
OnScrollChangedListenernever fires, so the tracker cannot detect visibility changes.Autoplay continues even though the view has been scrolled off-screen.
Recommendation Summary
NestedScrollView (traditional View)
Approach 2 (automatic) — simplest setup
Compose Column with verticalScroll
Approach 2 (automatic) — generally works
LazyColumn / LazyRow
Approach 1 (manual) — required because views are disposed
HorizontalPager / VerticalPager
Approach 1 (manual) — scroll events may not propagate
Complex nested Compose layouts with clipping
Approach 1 (manual) — getGlobalVisibleRect may be inaccurate
General rule of thumb:
For simple layouts, prefer Approach 2 for its simplicity.
For
LazyColumn/LazyRowscenarios, always use Approach 1 or avoidLazyColumnentirely by using a regular scrollableColumn.When in doubt, Approach 1 is the safest choice since it gives you full control over the visibility detection logic.
Last updated
Was this helpful?