Scrolling
This document will guide through the proper implementation of a scrollable list of videos - a task
typically achieved by using RecyclerView
s in Android.
Use playlists
The first and most important point is that Playlist
s 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.