Skip to main content
Version: 1.2.0

Scrolling

This document will guide through the proper implementation of a scrollable list of videos - a task typically achieved by using RecyclerViews in Android.

Use playlists#

The first and most important point is that Playlists should be used. When a collection of videos is wrapped in a playlist:

  • video content is cached
  • videos are preloaded based on heuristic depending on how the playlist is accessed
  • for paged playlists, new pages are automatically opened

This ensures the best performance during fast swiping, so it is highly recommended over lists of videos. Even when all you have is video IDs, you can create a custom playlist with them. For example, let's imagine that you have a post table in your database which stores the video ID, and a function to retrieve a list of posts:

class Post(val id: String, val videoId: String, val text: String, val user: String)
fun getPosts(): List<Post> { ... }

You can create a custom playlist as follows:

val posts = getPosts()
val ids = posts.map { it.id }
val request = CustomPlaylistRequest.build {
ids(*ids.toTypedArray())
}
val playlist = VideoKit.videos().getPlaylist(request).onSuccess {
// Got playlist!
}

Now the playlist can be passed to your recycler view's adapter.

Listen to playlist changes#

Playlists are smart objects that hold dynamic data. The underlying dataset can change for several reasons, for example if new videos are uploaded, old videos are deleted, or if a CustomPlaylist is modified using add/remove functionality.

In all these cases, it is important to observe changes in the dataset and notify the adapter immediately:

class Adapter(lifecycle: LifecycleOwner, playlist: Playlist): RecyclerView.Adapter<Item>() {
init {
playlist.addListener(lifecycle, object : PlaylistListener {
override fun onVideoInserted(id: String, index: Int) = notifyItemInserted(index)
override fun onVideoChanged(id: String, index: Int) = notifyItemChanged(index, id)
override fun onVideoRemoved(id: String, index: Int) = notifyItemRemoved(index)
override fun onVideoMoved(id: String, fromIndex: Int, toIndex: Int) {
notifyItemMoved(fromIndex, toIndex)
}
})
}
override fun getItemCount(): Int {
return playlist.size
}
}

Avoid change animations#

In the example above, we have called adapter.notifyItemChanged(index, payload), passing a payload object. This is important because it tells the RecyclerView to avoid 'change' animations.

In these animations, the recycler will use two separate views and bind them to the same data, while animating them in / out. This does not play very well with the player, who needs to allocate resources that shouldn't be bound to multiple views at the same time. We recommend to always use the adapter.notifyItemChanged(index, payload) signature and passing a non-null payload (like the video id).

Set (and reset!) videos#

Each post will be shown in its own UI piece held by the RecyclerView.ViewHolder. In order to be able to play the actual video, you can use our PlayerView (and optionally, PlayerControls):

class Item(view: View, lifecycle: LifecycleOwner): RecyclerView.ViewHolder(view) {
private val player: PlayerView = itemView.findViewById(R.id.player_view)
private val controls: PlayerControls = itemView.findViewById(R.id.player_controls)
init {
controls.setPlayer(player)
player.bind(lifecycle)
}
fun bind(video: Video) {
player.set(video, play = false)
}
fun unbind() {
player.reset()
}
fun play() {
player.play()
}
}

As you can image, player.set will be called when binding the view holder. However, it is also extremely important that you call player.reset when the holder is unbound. This helps with resource management and if you don't do so, playback might fail.

The adapter does not offer a handy callback, but we can use onViewRecycled and onFailedToRecycleView:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Item {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
return Item(view, lifecycle)
}
override fun onBindViewHolder(holder: Item, position: Int) {
holder.bind(playlist.getVideo(position))
}
override fun onViewRecycled(holder: Item) {
super.onViewRecycled(holder)
holder.unbind()
}
override fun onFailedToRecycleView(holder: Item): Boolean {
holder.unbind()
return false
}

Autoplay videos on scroll#

The RecyclerView scroll callback can be used to autoplay videos as soon as they are scrolled in. You are free to implement your own logic, but a simple implementation could be this:

private val scrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
maybePlay(recyclerView)
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
maybePlay(recyclerView) // Important for first layout
}
private fun maybePlay(recyclerView: RecyclerView) {
if (recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
val manager = recyclerView.layoutManager!! as LinearLayoutManager
val position = manager.findFirstCompletelyVisibleItemPosition()
if (position >= 0) {
val holder = recyclerView.findViewHolderForAdapterPosition(position) as? Item
holder?.play()
}
}
}
}

This listener can be registered in onAttachedToRecyclerView and unregistered in onDetachedFromRecyclerView. Note that you don't have to worry about pausing the previous holder after holder.play: this is automatically managed by the SDK, which will not allow two playbacks at the same time.

However, you might want to implement your own logic to pause playback when the video view is not completely visible, for example, or other app-specific behaviors.

Last updated on