Toro 101 - Videos on RecyclerView, starts/pauses automatically when scroll
Updated 2020/06/28: if you are interested in a better way to use ExoPlayer in RecyclerView, please give a look at Kohii. There is also a tutorial for this use case and for other use cases as well.
1. Intro
With the effort I put on to rework the Toro library, I believe that implementing auto playback playlist is easier than ever before. But with the lack of documentation skill, I'm afraid a single README or a few sample code cannot tell that to all the people who need it. So I write about it, what it does, how it can help you and how to use it with the least effort.
2. Our target
This post will show you how to use Toro v3 (currently beta 1) to implement a dead simple Video list, that will start and pause the playback automatically. A demo looks like below:
Question 1: Why do I even need to do this?
Answer 1: Only if you want to implement things like Twitter timeline, Facebook feed, or Instagram, where Video starts playback automatically when user scrolls to it, and pause it when user scrolls it off screen.
Question 2: This doesn't look like Facebook. Is it too simple?
Answer 2: First thing first. Future post will be about more complicated things, like Facebook timeline and so on. Of course you can skip this and go to the repo, check out the sample code. It is all there.
3. Before we start
Make sure the following things are in your checklist, and checked:
- You are creating an App targeting Android 16 or above. Because Toro officially supports ExoPlayer (2) which in turn supports Android downto 16, and also many advance features in Media playback start from this API onward.
- You have at least basic understanding about
RecyclerView
and how to use it. Because Toro is created on top ofRecyclerView
and its eco-system, a smallest knowledge about using it is required. - You own the Videos, or at least you acknowledge your right to use them.
4. The tutorial: step-by-step to create that cool Video list.
- First: Create your app using Android Studio if you have not done yet. Make sure you have a project to work on :trollface:. If you use some Video from the Internet for this sample, please make sure to include the permission to connect to the Internet into your
manifest.xml
. - Add
Toro
andExoPlayer
to your app module'sbuild.gradle
dependencies. Note that I also addCardView
dependency to improve our UI.
dependencies {
// ↑ other dependencies ...
// ↓ toro's dependencies and CardView
compile "im.ene.toro3:toro:3.0.0-beta1"
compile "com.google.android.exoplayer:exoplayer:r2.4.3"
compile "com.android.support:cardview-v7:26.0.0-beta2"
}
- We will now setup a Video list in our Activity using the
Container
as below:
<im.ene.toro.widget.Container
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/player_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
public class MainActivity extends AppCompatActivity {
Container container;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
container = findViewById(R.id.player_container);
}
}
We will complete this Activity later.
- Next we will design our Video player view. This time I use a single
SimpleExoPlayerView
for our Video player. Naming the layout file asview_holder_exoplayer_basic.xml
, the xml will look like below:
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/cardview_light_background"
>
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="200dp"
app:resize_mode="fixed_height"
app:surface_type="texture_view"
app:use_controller="false"
/>
</android.support.v7.widget.CardView>
One thing should be noticed here: SimpleExoPlayerView
uses a TextureView
or SurfaceView
to display the Video. By default, if its height or width is 0, your Surface/SurfaceTexture will not be created. Therefore your Video will not be played. To work around this, I set a minHeight
for it to be 200dp
which is reasonable for a 16:9
Video in portrait mode.
Also, related to the initial height of the PlayerView, my advice is to pre-calculate that height and set it to your layout so that your layout will not be resize at runtime, which gives a better UX.
In this tutorial, I use TextureView
for the SimpleExoPlayerView
.
Now let use that layout to create a ViewHolder
(which implements ToroPlayer
).
At first, your ViewHolder
will look like this:
public class SimplePlayerViewHolder extends RecyclerView.ViewHolder implements ToroPlayer {
SimpleExoPlayerView playerView;
public BasicPlayerViewHolder(View itemView) {
super(itemView);
playerView = (SimpleExoPlayerView) itemView.findViewById(R.id.player);
}
// some un-implemented methods below
}
Now since we are using SimpleExoPlayerView
, Toro comes with official support for this component, via SimpleExoPlayerViewHelper
. SimpleExoPlayerViewHelper
requires a Uri
to work, so we will have 2 more fields and now we can complete the implementation of ViewHolder
as below:
public class SimplePlayerViewHolder extends RecyclerView.ViewHolder implements ToroPlayer {
SimpleExoPlayerView playerView;
SimpleExoPlayerViewHelper helper;
Uri mediaUri;
public BasicPlayerViewHolder(View itemView) {
super(itemView);
playerView = (SimpleExoPlayerView) itemView.findViewById(R.id.player);
}
@Override public View getPlayerView() {
return playerView;
}
@Override public PlaybackInfo getCurrentPlaybackInfo() {
return helper != null ? helper.getLatestPlaybackInfo() : new PlaybackInfo();
}
@Override
public void initialize(Container container, PlaybackInfo playbackInfo) {
if (helper == null) {
helper = new SimpleExoPlayerViewHelper(container, this, mediaUri);
}
helper.initialize(playbackInfo);
}
@Override public void play() {
if (helper != null) helper.play();
}
@Override public void pause() {
if (helper != null) helper.pause();
}
@Override public boolean isPlaying() {
return helper != null && helper.isPlaying();
}
@Override public void release() {
if (helper != null) {
helper.release();
helper = null;
}
}
@Override public boolean wantsToPlay() {
return ToroUtil.visibleAreaOffset(this, itemView.getParent()) >= 0.85;
}
@Override public int getPlayerOrder() {
return getAdapterPosition();
}
void bind(Uri media) {
this.mediaUri = media;
}
}
It is worth mentioning that a correct implementation of ToroPlayer
must provide a good result of wantsToPlay()
, by which Container
will know when a Player should start playback or not. In most of my sample, I use ToroUtil.visibleAreaOffset()
which will calculate the visibility offset of the Video (not the whole View). You can always have a custom implementation (for example: Internet connected, User hit like, ...). Always returning false
will cause to no Video will be played. On the other hand, always returning true
doesn't guarantee that all of the Video will be played. I will talk more about this in future posts.
- The setup is almost done. We now only need an
Adapter
that uses theSimplePlayerViewHolder
above and finish our setup for theContainer
.
public class SimpleAdapter extends RecyclerView.Adapter<SimplePlayerViewHolder> {
MediaList mediaList = new MediaList();
@Override public SimplePlayerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.view_holder_exoplayer_basic, parent, false);
return new SimplePlayerViewHolder(view);
}
@Override public void onBindViewHolder(SimplePlayerViewHolder holder, int position) {
holder.bind(Uri.parse("my_awesome_video.mp4") /* FIXME use real data */);
}
@Override public int getItemCount() {
return mediaList.size();
}
}
public class MainActivity extends AppCompatActivity {
Container container;
SimpleAdapter adapter;
LinearLayoutManager layoutManager;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
container = findViewById(R.id.player_container);
layoutManager = new LinearLayoutManager(this);
container.setLayoutManager(layoutManager);
adapter = new SimpleAdapter();
container.setAdapter(adapter);
}
}
- Now try to launch your App and see what happens. If nothing goes wrong, your list now starts the playback automatically and it also correctly switch the Video on your scroll as well.
All the code above can be found in basic demo (this package includes more classes for ViewPager
demo too. For now you can safely ignore BasicListFragment
class).
5. Conclusion and future work
So here we are. With just a few code, we solve one of the most difficult UX in Android app. And it doesn't stop right there. In future post, I would like to introduce more complicated implementation using Toro, with ease. All of the code is already available on github/eneim/toro, I will have more or less in-depth discussions as well as my thought about why and how here.
All feedback are welcome. Happy scrolling!