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.
Clips are delivered in a 9:16 aspect ratio. While
StorytellerClipsFragmentwill do content cropping in a way that all given space is taken by the content (all area is consumed, cropping is applied to avoid content distortion). If you want to present content without cropping,StorytellerClipsFragmentshould reside in a container that maintains a 9:16 aspect ratio.
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 the dimensions' proportions so the clip content is not cropped at all. 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="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="9:16"
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:
- Create a new fragment instance using the
StorytellerClipsFragment.create(collectionId: String, context: StorytellerAnalyticsContext)method. a. Optionally, you can also set theinitialCategoryproperty to a string value to automatically navigate to a selected category. - Create a fragment transaction that would add this fragment to the fragment container view.
- 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 = hashMapOf("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 = hashMapOf("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.
val storytellerClipsFragment = StorytellerClipsFragment.create(
collection = "yourCollectionId",
topLevelBackEnabled = true,
context = hashMapOf("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?, dataCount: Int){
// 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 = hashMapOf("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 = hashMapOf("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 = hashMapOf("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="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="9:16"
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.
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.
val storytellerClipsFragment = StorytellerClipsFragment.create(
collection = "yourCollectionId",
topLevelBackEnabled = true,
context = hashMapOf("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?, dataCount: Int){
// will be invoked when data load completes or fails
}
}
storytellerClipsFragment.reloadData()
Inset Management#
One of the usecase of StorytellerClipsFragment is to embed it in a view that is laid behind status bar.
While that will increase size of the screen available for the content,
this arrangement can potentially causing the clip title and other UI elements to be obscured by the status bar.
To avoid this problem you can set StorytellerClipsFragment.topInset property to the height of the status bar.
This will cause the fragment to offset the title and the top buttons by the given amount of pixels.
Similarly if the bottom part of the view is obscured by the Navbar, you can set StorytellerClipsFragment.bottomInset property to the height of the navigation bar.
The recommended approach is to use this property together with onApplyWindowInsets callback.
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
}
Compose Integration#
StorytellerClipsFragment can be used in Jetpack Compose using the StorytellerEmbeddedClips composable.
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 = hashMapOf("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)
}
}
}
}
StorytellerEmbeddedClipsState#
rememberStorytellerEmbeddedClipsState is a composable function that creates a StorytellerEmbeddedClipsState object that holds the state of the StorytellerEmbeddedClips composable.
val state = rememberStorytellerEmbeddedClipsState(
collectionId = "collection",
topLevelBack = topLevelBack,
context = hashMapOf("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 = hashMapOf("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 = hashMapOf("placementId" to "embedded_clips_compose", "location" to "ClipsScreen")
)
state.goBack() // navigate to previous category programmatically