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:

  1. An OnScrollChangedListener is registered on the NestedScrollView's ViewTreeObserver.

  2. On each scroll event (throttled to every 100ms), getGlobalVisibleRect() computes how much of the FwPlayerDeckView is on screen.

  3. When the visible ratio drops below 50%, onViewPortLeft() pauses playback.

  4. 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:

  1. rememberScrollState() tracks the scroll position of the Column. Reading scrollState.value in the composable body triggers recomposition whenever the scroll position changes.

  2. Critical: currentScrollValue is referenced inside the update lambda. 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 captures lastVisibilityState (a MutableState object whose reference never changes), so Compose considers the lambda unchanged and skips calling update entirely — even though the parent composable recomposed.

  3. In the update block, getGlobalVisibleRect() computes the visible portion of the AndroidView on screen (consistent with Demo 1A and the SDK's internal tracker).

  4. The lastVisibilityState guard prevents redundant onViewPortEntered() / onViewPortLeft() calls — only actual state transitions trigger notification.

  5. 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:


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 (with Modifier.verticalScroll()), not LazyColumn. 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:

  1. Listeners registered: OnGlobalLayoutListener and OnScrollChangedListener on the view's ViewTreeObserver. An OnAttachStateChangeListener handles window attach/detach events.

  2. 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:

  3. Threshold: If visibleFraction >= 0.5 (50%), the view is considered visible. If it drops below 50%, the view is considered hidden.

  4. Callback: When the visibility state changes, the tracker notifies the FwPlayerDeckView, which calls playerManager.pauseTemporarily() or playerManager.resumeFromTemporaryPause() accordingly.

  5. 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 ViewVisibilityTracker never 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 onRelease callback fires on every scroll-out, calling destroy(), and factory fires on every scroll-in, calling init() 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.offset with 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 OnScrollChangedListener never fires, so the tracker cannot detect visibility changes.

  • Autoplay continues even though the view has been scrolled off-screen.


Recommendation Summary

Scenario
Recommended Approach

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/LazyRow scenarios, always use Approach 1 or avoid LazyColumn entirely by using a regular scrollable Column.

  • 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?