Toro 3.0 alpha 1 was born, from scratch with love

25 June 2017
#Dev#EN#Toro#Android#RecyclerView#ExoPlayer#MediaPlayer

TL,DR:

Just in case you are interested in Video playback, especially in RecyclerView, there is something new for you: https://github.com/eneim/toro/releases/tag/3.0.0-alpha1

A demonstration (mimic Facebook timeline and more)


Intro

After nearly a month of active development, I would like to announce that Toro has a new major update: Toro 3.0 (alpha 1). Toro 3.0 is a complete rewritten version of Toro.

Update summary

Toro 3.0 removes the use of static setup in Toro 2 (Toro.attach/detach/init/register/unregister ...), which was a burden for both maintainer and users.

Instead, all the necessary logic is put into Container - an empowered RecyclerView. In fact, in the RecyclerView eco-system (RecyclerView, Adapter, LayoutManager, ViewHolder, ...), RecyclerView is the least likely to be customised (comparing to Adapter and LayoutManager). With that, I put my effort to integrate the beauty of Toro 2.x into a single RecyclerView. As a result, user just need to replace their RecyclerView with Container view to have the support from Toro.

<!-- just this and you have all the support from Toro 3.0 -->
<im.ene.toro.widget.Container
    android:id="@+id/recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
/>

Also, there is no need to implement the MediaPlayerManager which is tight to Adapter in Toro 2. I was struggling with this constraint for a long time after the release, and finally I know it must be removed. In Toro 3, there is no customisable MediaPlayerManager, its logic is instead integrated inside Container.

What user still need to do, is to implement the Player interface into the ViewHolder. This is a must, since there is no other way the Container could know if it is talking to a Player or not. But instead of dozens of methods in Toro 2.x, the ToroPlayer interface now contains only a few methods which are enough to control the playback, and few more for the callback from the playback update. A simple ViewHolder implementation looks like below

public class TimelineVideoViewHolder extends RecyclerView.ViewHolder implements ToroPlayer {

  @Nullable SimpleExoPlayerViewHelper helper;
  @Nullable private Uri mediaUri;

  @BindView(R.id.fb_video_player) SimpleExoPlayerView playerView;
  
  TimelineVideoViewHolder(View itemView) {
    super(itemView);
    ButterKnife.bind(this, itemView);
    playerView.setVisibility(View.VISIBLE);
  }

  @Override void bind(TimelineAdapter adapter, FbItem item, List<Object> payloads) {
    super.bind(adapter, item, payloads);
    if (item != null && item instanceof MediaEntity) {
      MediaUrl url = ((MediaEntity) item).getMediaUrl();
      mediaUri = url.getUri();
    }
  }

  @NonNull @Override public View getPlayerView() {
    return this.playerView;
  }

  @NonNull @Override public PlaybackInfo getCurrentPlaybackInfo() {
    return helper != null ? helper.updatePlaybackInfo() : new PlaybackInfo();
  }

  @Override
  public void initialize(@NonNull Container container, @NonNull 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) {
      try {
        helper.cancel();
      } catch (Exception e) {
        e.printStackTrace();
      }
      helper = null;
    }
  }

  @Override public boolean wantsToPlay() {
    ViewParent parent = itemView.getParent();
    float offset = 0;
    if (parent != null && parent instanceof View) {
      offset = ToroUtil.visibleAreaOffset(playerView, (View) parent);
    }
    return offset >= 0.85;
  }

  @Override public int getPlayerOrder() {
    return getAdapterPosition();
  }
}

This release also open a way for multiple simultaneous playbacks, even though I would not recommend it. Now with a custom PlayerSelector, you can have the behavior like below: different number of Video playback for each row.

Deep down to the implementation, Toro 3.0 is no longer listen to Playback state. In fact, it was a pain in my ass when I try to follow the state machine of 3 Media player APIs (MediaPlayer, ExoPlayer 1, ExoPlayer 2) to tell Toro instance to deal with a Player when it is ready to play or when it completes the playback. In Toro 3.0, it is now just asking the Player to initialize the resource for playback, and then start it, or pause it, on demand. All the playback state update is ignore by Toro, and users will have the freedom to integrate their own callback logic.

This change will asks the users more to handle the callback by themselves, which can be hard at first. So I put 2 helpers classes to help them getting start with Toro 3.0 easier. Right now only ExoPlayer 2.+ and Android MediaPlayer are supported. And there will be no plan to support more playback SDKs. But as users will need to implement 3rd party SDK such as Youtube, Vimeo, etc, I'm always try to help as much as possible.

More detail about how to use Toro 3.0 alpha 1 can be found in README as well as the sample app. So it is highly recommended to go though the README as well as app module before asking for any implementation details.

What happens with Toro 2.x: I will try to answer as much as possible, and fix fatal issues only. All up coming development will go to 3.+ and so on.

Final words

I would like to write more about this release, as well as what I have learnt about RecyclerView while creating this and more. But for now, I will take a rest and wait for issue report.

Happy scrolling!