Skip to content

StorytellerClipsFragment#

StorytellerClipsFragment is a fragment that can be used to embed a clip in your own activities. Like a regular Android Fragment, it can be configured from an XML layout or instantiated programmatically and attached to the host's (another fragment or activity) FragmentManager via a fragment transaction.

Embedded Clips fill the host container. Size the container according to your app's layout and keep that size stable during refreshes to avoid layout shift. If the content and container dimensions differ, cropping may be applied to avoid distortion.

Showcase examples#

Using StorytellerClipsFragment from xml layout#

StorytellerClipsFragmentcan be used directly from the layout XML files. The usage is identical to typical Android fragments. You need to specify the fully qualified fragment class name in the android:name property and the collection ID using app:storyteller_collection_id_property. Note that in this example, a constraint is applied to reserve stable space for the embedded player. app:storyteller_initial_category is optional and can be used to set the initial category of the collection to be viewed. If this is used. The clip will be first loaded with the collection. The selected category will then be navigated to automatically.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent" android:layout_height="match_parent">

  <androidx.fragment.app.FragmentContainerView
    android:id="@+id/fragment_host"
    android:name="com.storyteller.ui.pager.StorytellerClipsFragment"
    android:layout_width="match_parent"
    android:layout_height="400dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:storyteller_collection_id="your-collection-id"
    app:storyteller_initial_category="initial-category-of-collection"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Using StorytellerClipsFragment programmatically#

StorytellerClipsFragment can also be used programmatically. To embed a clip fragment programmatically, you need to follow these steps:

  1. Create a new fragment instance using the StorytellerClipsFragment.create(collectionId: String, context: StorytellerAnalyticsContext) method. a. Optionally, you can also set the initialCategory property to a string value to automatically navigate to a selected category.
  2. Create a fragment transaction that would add this fragment to the fragment container view.
  3. Commit the created transaction.

See the following snippet illustrating attaching a fragment in the Activity.onCreate method:

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  // An example using viewBinding feature; see view binding for reference.
  val binding = ActivityClipFragmentHostBinding.inflate(layoutInflater)
  setContentView(binding.root)

  val storytellerClipsFragment = StorytellerClipsFragment.create(
    collectionId = "yourCollectionId",
    context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen")
  )
  val transaction = supportFragmentManager.beginTransaction()
  transaction.add(
    binding.fragmentHost.id,
    storytellerClipsFragment
  )
  transaction.commit()
}

Controlling playback in StorytellerClipsFragment#

StorytellerClipsFragment by default starts playback as soon as it is attached to the host and visible. Standard Android Fragment lifecycle events typically handle automatic pausing and resuming (e.g., when the app goes to the background), complementing the manual control provided by the shouldPlay property.

// finding the fragment by id using the fragment manager
 val storytellerClipsFragment = supportFragmentManager.findFragmentById(binding.fragmentHost.id)
    as StorytellerClipsFragment

storytellerClipsFragment.shouldPlay = false // stops playback
storytellerClipsFragment.shouldPlay = true // starts playback

StorytellerClipsFragment also contains canGoBack:Boolean property which can be used to check if the fragment can go back from the current Category or is it at the top level.

  val storytellerClipsFragment = StorytellerClipsFragment.create(
    collection = "yourCollectionId",
    topLevelBackEnabled = true,
    context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen")
  )
  val canGoBack = storytellerClipsFragment.canGoBack

StorytellerClipsFragment also contains listener property which can be used to control playback and handle top level back button press. Data-load callbacks are emitted for the top-level Clips collection load. Following feed loads do not trigger onDataLoadStarted or onDataLoadComplete, and Embedded Clips completion does not include dataCount.

  val storytellerClipsFragment = StorytellerClipsFragment.create(
    collection = "yourCollectionId",
    topLevelBackEnabled = true,
    context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen")
  )
  storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener {
    override fun onTopLevelBackPressed(): Boolean {
      // will be invoked when top level back button is pressed
      return true // true if fragment needs to be stopped / host should handle back
    }

    override fun onDataLoadStarted() {
      // will be invoked when data load starts
    }

    override fun onDataLoadComplete(success: Boolean, error: Error?) {
      // will be invoked when data load completes or fails
    }
  }
  storytellerClipsFragment.reloadData()

External back button handling#

StorytellerClipsFragment does not show the top level back button by default. You can control the behaviour by setting topLevelBackEnabled property and setting onTopLevelBackPressed according to your needs.

  val storytellerClipsFragment = StorytellerClipsFragment.create(
    collection = "yourCollectionId",
    topLevelBackEnabled = true,
    context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen")
  )
  storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener {
    override fun onTopLevelBackPressed(): Boolean {
      // will be invoked when top level back button is pressed
      return true // true if fragment needs to be stopped
    }
  }

StorytellerClipsFragment also contains canGoBack:Boolean property which can be used to check if the fragment can go back from the current Category or is it at the top level.

  val storytellerClipsFragment = StorytellerClipsFragment.create(
    collection = "yourCollectionId",
    topLevelBackEnabled = true,
    context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen")
  )
  val canGoBack = storytellerClipsFragment.canGoBack

Initial Category#

To set the StorytellerClipsFragment to start with a specific category, you can set the initialCategory property.

Programmatical Usage

  val storytellerClipsFragment = StorytellerClipsFragment.create(
    collection = "yourCollectionId",
    initialCategory = "yourCategory",
    context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen")
  )

XML Usage

  <androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_host"
    android:name="com.storyteller.ui.pager.StorytellerClipsFragment"
    android:layout_width="match_parent"
    android:layout_height="400dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:storyteller_collection_id="your-collection-id"
    app:storyteller_initial_category="initial-category-of-collection"/>

If the category is found in the collection, the clip will be loaded with the collection and the selected category. The category will be navigated to automatically. If the category is not found in the collection or is invalid, it will be ignored.

Ad Placement Controls#

Embedded Clips supports per-presentation ad control through StorytellerClipsAdConfiguration.

Embedded bottom banners are opt-in across every embedding entry point — rememberStorytellerEmbeddedClipsState, StorytellerEmbeddedClipsState, and both StorytellerClipsFragment.create(...) overloads. Pass an adConfiguration — directly, or via StorytellerClipCollectionConfiguration.adConfiguration — to enable the banner. A StorytellerClipCollectionConfiguration with no adConfiguration (null) keeps the bottom banner disabled for embedding; full-screen Storyteller.openCollection enables it when the configuration is omitted.

Embedded Clips continue to suppress the standard zero-index opening ad-as-Clip. When a correctly configured StorytellerImaModule is registered, preRollEnabled = true permits one true IMA pre-roll before the initially opened Embedded Clip; set it to false for a user who should not receive that pre-roll. betweenClipsAdProviderOrder controls the ordered allowlist for later standard ad slots, while bottomBannerEnabled controls only bottom banners. Nullable frequency and initialIndex values independently inherit remote cadence; valid supplied values override later cadence for this Embedded presentation. See Per-presentation Clips Ad Controls.

val storytellerClipsFragment = StorytellerClipsFragment.create(
  configuration = Storyteller.StorytellerClipCollectionConfiguration(
    collectionId = "yourCollectionId",
    adConfiguration = Storyteller.StorytellerClipsAdConfiguration(
      bottomBannerEnabled = true,
      preRollEnabled = false,
      betweenClipsAdProviderOrder = listOf(
        Storyteller.StorytellerAdProvider.VAST,
        Storyteller.StorytellerAdProvider.GAM,
      ),
      frequency = 4,
      initialIndex = 1,
    ),
  ),
)

Reload Data#

StorytellerClipsFragment in order to reload data you can call reloadData() method. This method will make a request to the backend to fetch the latest data.

If this method is called when there are category filters applied, then it will go back one level. If there are no category filters applied this method will reload data.

Loading state can be observed by setting listener property and overriding it's onDataLoadStart and onDataLoadComplete methods. These loading callbacks apply to the top-level Clips collection load. Following feed loads do not trigger Embedded Clips loading callbacks.

  val storytellerClipsFragment = StorytellerClipsFragment.create(
    collection = "yourCollectionId",
    topLevelBackEnabled = true,
    context = mapOf("placementId" to "embedded_clips", "location" to "ClipsScreen")
  )
  storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener {
    override fun onTopLevelBackPressed(): Boolean {
      // will be invoked when top level back button is pressed
      return true // true if fragment needs to be stopped / host should handle back
    }

    override fun onDataLoadStarted() {
      // will be invoked when data load starts
    }

    override fun onDataLoadComplete(success: Boolean, error: Error?) {
      // will be invoked when data load completes or fails
    }
  }
  storytellerClipsFragment.reloadData()

Inset Management#

Use topInset and bottomInset only when the Embedded Clips container is deliberately laid behind an occluding system surface. For example, topInset can offset the title and top controls when the container extends behind the status bar. If the host has already consumed that system inset by padding or sizing the container, leave the corresponding Storyteller inset at 0 so it is not applied twice.

The Clips Instructions screen uses these same insets for its content. The Tap to Start button stays visible without scrolling, while the heading and instruction rows scroll above it on compact layouts. This applies to both Compose Embedded Clips and StorytellerClipsFragment.

These properties are not the integration seam for sibling host UI such as a clickable app bottom navigation bar. Size the Embedded Clips container so its bottom edge ends at the top of that navigation instead; see Host navigation and bottom banners.

ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, winInsets ->
  val inset =
    winInsets.getInsets(
      WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()
    )

  storytellerClipsFragment.topInset = inset.top
  storytellerClipsFragment.bottomInset = inset.bottom

  WindowInsetsCompat.CONSUMED
}

Do not also apply the same values as container padding. Choose one owner for each system inset.

Host navigation and bottom banners#

Embedded Clips fill the bounds supplied by the host. When the screen also contains clickable bottom navigation, make the Storyteller container end at the navigation's top edge. Storyteller owns layout inside those local bounds; the host owns the navigation component and system bars. From Android SDK 11.7.0, effective-bottom Embedded Clips use a compact 8dp progress region matching the track's maximum visual height; no larger scrub target is reserved for this arrangement. When a width-constrained video creates enough measured black space below the media for the banner and its 16dp lower band, the progress track uses its top edge at the approved position 16dp before the banner. Without qualifying bottom letterbox space, the current banner-before-progress order is preserved and the bottom-edge track remains at the local container boundary. Caller navigation and system navigation remain outside the measured letterbox, so do not add their heights as SDK padding. Clips without a banner, direct/modal Clips, and eligible Embedded ABOVE_ACTION layouts retain the existing 80dp target. A height-constrained host retains the legacy layout rather than hiding an eligible banner.

Do not add the host navigation height to StorytellerEmbeddedClipsState.bottomInset or StorytellerClipsFragment.bottomInset after excluding it from the container. Doing both creates double padding.

Full-height Compose host#

Use the Scaffold content bounds for StorytellerEmbeddedClips. The bottom bar remains a sibling outside the SDK container:

val state = rememberStorytellerEmbeddedClipsState(
  collectionId = "your-collection-id",
  topLevelBack = false,
  adConfiguration = Storyteller.StorytellerClipsAdConfiguration(
    bottomBannerEnabled = true,
  ),
)

Scaffold(
  bottomBar = { HostBottomNavigation() },
) { contentPadding ->
  Box(
    modifier = Modifier
      .fillMaxSize()
      .padding(contentPadding)
      .consumeWindowInsets(contentPadding),
  ) {
    StorytellerEmbeddedClips(
      modifier = Modifier.fillMaxSize(),
      state = state,
    )
  }
}

Full-height Fragment host#

Constrain the fragment container above the host navigation. The Fragment fills only the resulting local bounds:

<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <androidx.fragment.app.FragmentContainerView
    android:id="@+id/storyteller_clips_container"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@id/host_bottom_navigation"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/host_bottom_navigation"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Apply gesture-navigation or three-button system insets to the host navigation or its parent exactly once. Keep bottomInset = 0 when those system bounds are already outside the fragment container.

Compose Integration#

StorytellerClipsFragment can be used in Jetpack Compose using the StorytellerEmbeddedClips composable. You can optionally pass clipId to start the embedded player from a specific Clip in the collection. This is useful when the host app has a Storyteller Clip ID for a selected content item, such as a live-blog post or match-centre event.

class DemoComposeEmbeddedClipsActivity : FragmentActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val collection = intent.getStringExtra("collection") ?: ""
    val widthPercentage = intent.getIntExtra("width", 100) / 100F
    val heightPercentage = intent.getIntExtra("height", 80) / 100F
    val topLevelBack = intent.getBooleanExtra("topLevelBack", false)
    val initialCategory = intent.getStringExtra("category") ?: ""
    enableEdgeToEdge()
    setContent {
      val state = rememberStorytellerEmbeddedClipsState(
        collectionId = collection,
        topLevelBack = topLevelBack,
        initialCategory = initialCategory,
        context = mapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen")
      )
      StorytellerEmbeddedClips(
        modifier = Modifier
          .fillMaxWidth(widthPercentage)
          .fillMaxHeight(heightPercentage),
        state = state,
      )
    }
  }

  companion object {
    fun start(context: Context, collection: String, category: String?, width: Int, height: Int, topLevelBack: Boolean) {
      // add params to intent
      Intent(context, DemoComposeEmbeddedClipsActivity::class.java).apply {
        putExtra("collection", collection)
        putExtra("category", intialCategory)
        putExtra("width", width)
        putExtra("height", height)
        putExtra("topLevelBack", topLevelBack)
        context.startActivity(this)
      }
    }
  }
}

Starting from a specific Clip in Compose#

Use the clipId parameter when the host app should open Embedded Clips on a specific Clip inside the collection.

val state = rememberStorytellerEmbeddedClipsState(
  collectionId = "your-collection-id",
  clipId = "your-clip-id",
  topLevelBack = true,
  context = mapOf(
    "placementId" to "embedded_clips",
    "location" to "LiveBlog"
  )
)

StorytellerEmbeddedClips(
  modifier = Modifier,
  state = state
)

If clipId is omitted, Embedded Clips starts from the first available Clip in the collection.

StorytellerEmbeddedClipsState#

rememberStorytellerEmbeddedClipsState is a composable function that creates a StorytellerEmbeddedClipsState object that holds the state of the StorytellerEmbeddedClips composable. It accepts an optional clipId. When provided, Embedded Clips starts playback from that Clip if it is available in the collection.

val state = rememberStorytellerEmbeddedClipsState(
  collectionId = "collection",
  topLevelBack = topLevelBack,
  adConfiguration = Storyteller.StorytellerClipsAdConfiguration(
    bottomBannerEnabled = false,
    frequency = 4,
    initialIndex = 1,
  ),
  context = mapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen")
)
StorytellerEmbeddedClips(
  modifier = Modifier,
  state = state
)

StorytellerEmbeddedClipsState contains canGoBack property that can be used to check if the fragment can go back from the current Category or is it at the top level.

val state = rememberStorytellerEmbeddedClipsState(
  collectionId = "collection",
  topLevelBack = topLevelBack,
  context = mapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen")
)

val canGoBack = state.canGoBack // true if user can navigate back

StorytellerEmbeddedClipsState contains goBack() which will move the content to previous Category if the user is not at the top level.

val state = rememberStorytellerEmbeddedClipsState(
  collectionId = collection,
  topLevelBack = topLevelBack,
  context = mapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen")
)

state.goBack() // navigate to previous category programmatically