Toro 101 - Videos on RecyclerView, starts/pauses automatically when scroll

08 July 2017
#Android#RecyclerView#MediaPlayer#ExoPlayer#Toro

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 of RecyclerView 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 and ExoPlayer to your app module's build.gradle dependencies. Note that I also add CardView dependency to improve our UI.
build.gradle
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:
activity_main.xml
<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"
    />
MainActivity.java
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 as view_holder_exoplayer_basic.xml, the xml will look like below:
view_holder_exoplayer_basic.xml
<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:

SimplePlayerViewHolder.java (incompleted)
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:

SimplePlayerViewHolder.java
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 the SimplePlayerViewHolder above and finish our setup for the Container.
SimpleAdapter.java
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();
    }
}
MainActivity.java
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!