Skip to content

7. Recipes

eneim edited this page Oct 25, 2016 · 16 revisions

Below are some tutorials to help users to implement their own components

Menu

7.1 Custom Playback Strategy
7.2 Custom Extension
7.3 Custom ViewHolder
7.4 Custom MediaPlayerManager
7.5 Custom LayoutManager (to use with Toro)

7.1 Custom Playback Strategy

Back to top

By implementing the interface ToroStrategy, apply your own logic in required methods, you can have your own Strategy. Built-in ones can be used as delegation. See below for a sample:

Least dependency

compile "im.ene.toro2:toro:2.1.0"
public class PlayFromTopStrategy implements ToroStrategy {

  private final int firstMediaPosition; // Should be the first Media obtained item from Adapter

  private boolean isFirstVideoObserved = false;
  private final ToroStrategy delegate = Toro.Strategies.FIRST_PLAYABLE_TOP_DOWN;

  public PlayFromTopStrategy(int firstMediaPosition) {
    this.firstMediaPosition = firstMediaPosition;
  }

  @Override public String getDescription() {
    return "All Media should be played by order, start from 'firstMediaPosition'";
  }

  @Nullable @Override public ToroPlayer findBestPlayer(List<ToroPlayer> candidates) {
    // Inherit the same behaviour with built-in one.
    return delegate.findBestPlayer(candidates);
  }

  @Override public boolean allowsToPlay(ToroPlayer player, ViewParent parent) {
    boolean allowToPlay = (isFirstVideoObserved || player.getPlayOrder() == firstMediaPosition)  //
        && delegate.allowsToPlay(player, parent);
    // Keep track of first video on top.
    if (player.getPlayOrder() == firstMediaPosition) {
      isFirstVideoObserved = true;
    }
    return allowToPlay;
  }
}

And use it as usual:

Toro.setStrategy(new PlayFromTopStrategy(2)); // I know that the 3rd item is a Video.

7.2 Create a Custom Extension

Back to top

Here is the tutorial about how to create your own Toro Extension.

As can be seen from the library structure, each extension is implemented in a separated way, which means that anyone can do the same thing. The following is a check list for those when building their own Extension for Toro:

  • A good motivation: built-in Extensions are not enough, there are better Media Playback APIs out there, ...

  • When you have a good motivation, now build your own PlayerView - a custom View which can be used to display the Media Content (well, just another VideoView, or SimpleExoPlayerView, ...). If you build your Extension on top of built-in one, it is good to use my built-in Views, or customize it by your own will.

  • Create the extension of PlayerViewHelper: as said, this class stays between the ViewHolder, the Media Playback API and Toro's internal API. A ViewHolder will require it to connect to all benefit of Toro as well as the Playback API. My built-in Helpers can be re-used as delegation as well.

  • Create the ViewHolder: this is the main character. You will need to create a ViewHolder and setup its PlayerViewHelper instance there, to connect your ViewHolder with your Playback API as well as Toro internal API.

  • Optional: extends the MediaPlayerManager. This can be useful when you need to have access to the Manager, or you have better way to handle the position cache (use native cache, maybe). See TimelineAdapter.java to see how I create a simple one.

7.3 Custom ViewHolder

Back to top

It is recommended that user implements Custom ViewHolder by extending ToroAdapter$$ViewHolder and implementing ToroPlayer or even ExtToroPlayer. Using built-in extensions, it will be more convenient. User can just extend built-in Base ViewHolder implementation. Below is an example, extract from Facebook Demo, shows you how to create one:

Least dependency

compile "im.ene.toro2:toro-ext-exoplayer2:2.1.0"

ViewHolder's itemView layout (R.layout.vh_fb_feed_post_video)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    >

  <View
      android:layout_width="match_parent"
      android:layout_height="@dimen/activity_vertical_margin"
      android:background="#e5e5e5"
      />

  <!-- external widget -->
  <include layout="@layout/vh_fb_feed_user"/>

  <TextView
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="#f5f5f5"
      android:lineSpacingMultiplier="1.2"
      android:padding="8dp"
      android:text="@string/sample"
      android:textAppearance="@style/TextAppearance.AppCompat.Body1"
      />

  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_margin="4dp"
      android:background="#20000000"
      android:padding="4dp"
      >

    <im.ene.toro.exoplayer2.ExoVideoView
        android:id="@+id/video"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:layout_centerInParent="true"
        android:layout_gravity="center"
        />

    <ImageView
        android:id="@+id/thumbnail"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignBottom="@id/video"
        android:layout_alignEnd="@id/video"
        android:layout_alignLeft="@id/video"
        android:layout_alignRight="@id/video"
        android:layout_alignStart="@id/video"
        android:layout_alignTop="@id/video"
        android:layout_centerInParent="true"
        android:background="#40ffffff"
        android:padding="16dp"
        android:scaleType="centerInside"
        />

    <TextView
        android:id="@+id/info"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:layout_margin="4dp"
        android:background="#80000000"
        android:gravity="center"
        android:includeFontPadding="false"
        android:paddingBottom="4dp"
        android:paddingEnd="8dp"
        android:paddingStart="8dp"
        android:paddingTop="4dp"
        android:maxLines="1"
        android:textAppearance="@style/TextAppearance.AppCompat.Small"
        android:textColor="@android:color/white"
        />

  </RelativeLayout>

  <View
      android:layout_width="match_parent"
      android:layout_height="0.5dp"
      android:layout_marginLeft="24dp"
      android:layout_marginRight="24dp"
      android:background="#e5e5e5"
      />

  <!-- external widget -->
  <include layout="@layout/vh_fb_feed_bottom"/>
</LinearLayout>

Preview

public class VideoViewHolder extends ExoVideoViewHolder {

  static final int LAYOUT_RES = R.layout.vh_fb_feed_post_video;

  // A ViewHolder needs an Object to bind its View. This is it.
  private TimelineItem.VideoItem videoItem;
  // Thumbnail view, stays above the Video.
  private ImageView mThumbnail;
  // Info view, show current playback state, stays at the right-bottom, above the Video and Thumbnail.
  private TextView mInfo;

  public VideoViewHolder(View itemView) {
    super(itemView);
    // TODO: Use ButterKnife, maybe?
    mThumbnail = (ImageView) itemView.findViewById(R.id.thumbnail);
    mInfo = (TextView) itemView.findViewById(R.id.info);
  }

  @Override protected ExoVideoView findVideoView(View itemView) {
    return (ExoVideoView) itemView.findViewById(R.id.video);
  }

  @Override public void bind(RecyclerView.Adapter adapter, @Nullable Object object) {
    // Make sure we are binding a VideoItem, not anything else.
    if (!(object instanceof TimelineItem)
        || !(((TimelineItem) object).getEmbedItem() instanceof TimelineItem.VideoItem)) {
      throw new IllegalArgumentException("Only VideoItem is accepted");
    }

    this.videoItem = (TimelineItem.VideoItem) ((TimelineItem) object).getEmbedItem();
    // Call the VideoView API
    this.videoView.setMedia(Uri.parse(videoItem.getVideoUrl()));
  }

  @Override public void setOnItemClickListener(View.OnClickListener listener) {
    super.setOnItemClickListener(listener);
    mInfo.setOnClickListener(listener);
    this.videoView.setOnClickListener(listener);
  }

  @Nullable @Override public String getMediaId() {
    return Util.genVideoId(this.videoItem.getVideoUrl(), getAdapterPosition());
  }

  @Override public void onVideoPreparing() {
    super.onVideoPreparing();
    mInfo.setText("Preparing");
  }

  @Override public void onVideoPrepared() {
    super.onVideoPrepared();
    mInfo.setText("Prepared");
  }

  @Override public void onViewHolderBound() {
    super.onViewHolderBound();
    // I thought I have Glide, too. But anyway ...
    Picasso.with(itemView.getContext())
        .load(R.drawable.toro_place_holder)
        .fit()
        .centerInside()
        .into(mThumbnail);
    mInfo.setText("Bound");
  }

  @Override public void onPlaybackStarted() {
    // Use animation to have a fancy UI
    mThumbnail.animate().alpha(0.f).setDuration(250).setListener(new AnimatorListenerAdapter() {
      @Override public void onAnimationEnd(Animator animation) {
        // Call from here, not outside
        VideoViewHolder.super.onPlaybackStarted();
      }
    }).start();
    mInfo.setText("Started");
  }

  @Override public void onPlaybackPaused() {
    // Use animation to have a fancy UI
    mThumbnail.animate().alpha(1.f).setDuration(250).setListener(new AnimatorListenerAdapter() {
      @Override public void onAnimationEnd(Animator animation) {
        // Call from here, not outside
        VideoViewHolder.super.onPlaybackPaused();
      }
    }).start();
    mInfo.setText("Paused");
  }

  @Override public void onPlaybackCompleted() {
    // Use animation to have a fancy UI
    mThumbnail.animate().alpha(1.f).setDuration(250).setListener(new AnimatorListenerAdapter() {
      @Override public void onAnimationEnd(Animator animation) {
        // Call from here, not outside
        VideoViewHolder.super.onPlaybackCompleted();
      }
    }).start();
    mInfo.setText("Completed");
  }

  @Override public boolean onPlaybackError(Exception error) {
    // Use animation to have a fancy UI
    mThumbnail.animate().alpha(1.f).setDuration(0).setListener(new AnimatorListenerAdapter() {
      @Override public void onAnimationEnd(Animator animation) {
        // Finish immediately, nothing to do.
      }
    }).start();
    mInfo.setText("Error: videoId = " + getMediaId());
    return super.onPlaybackError(error);
  }
}

If you really want to create your ViewHolder from scratch, just follow the way I create ExoVideoViewHolder and VideoViewHolder that extends it.

7.4 Custom MediaPlayerManager

Back to top

MediaPlayerManager API is the heart of Toro API. There is already a built-in one (the MediaPlayerManagerImpl) that is enough for almost everything. But in case Users want to have their own, make sure that you implement it by building a RecyclerView's Adapter, implement MediaPlayerManager. And it is recommended to have MediaPlayerManagerImpl as the delegation if possible.

Below is an example taken from Facebook Demo to demonstrate how I create a custom MediaPlayerManager:

Least dependency

compile "im.ene.toro2:toro:2.1.0"
public class TimelineAdapter extends ToroAdapter<ToroAdapter.ViewHolder>
    implements MediaPlayerManager, OrderedPlayList // A Video list that is sorted  {

  static final int TYPE_OGP = 1;
  static final int TYPE_PHOTO = 2;
  static final int TYPE_VIDEO = 3;

  private static final int ITEM_COUNT = 512;
  private final List<TimelineItem> items;
  private final MediaPlayerManager delegate;

  public TimelineAdapter() {
    this.delegate = new MediaPlayerManagerImpl();
    this.items = new ArrayList<>();
    for (int i = 0; i < ITEM_COUNT; i++) {
      items.add(new TimelineItem(ToroApp.getApp()));
    }
  }

  @NonNull @Override protected TimelineItem getItem(int position) {
    return items.get(position);
  }

  private ItemClickListener onItemClickListener;

  public void setOnItemClickListener(ItemClickListener onItemClickListener) {
    this.onItemClickListener = onItemClickListener;
  }

  @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    final ViewHolder viewHolder = TimelineViewHolder.createViewHolder(parent, viewType);
    if (viewHolder instanceof OgpItemViewHolder) {
      viewHolder.setOnItemClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
          int position = viewHolder.getAdapterPosition();
          if (position != RecyclerView.NO_POSITION
              && onItemClickListener != null
              && v == ((OgpItemViewHolder) viewHolder).ogpView) {
            onItemClickListener.onOgpItemClick(viewHolder, v,
                (TimelineItem.OgpItem) getItem(position).getEmbedItem());
          }
        }
      });
    } else if (viewHolder instanceof VideoViewHolder) {
      // TODO Click to Video
      viewHolder.setOnItemClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
          int position = viewHolder.getAdapterPosition();
          if (position != RecyclerView.NO_POSITION
              && onItemClickListener != null
              && v == ((VideoViewHolder) viewHolder).getPlayerView()) {
            onItemClickListener.onVideoClick(viewHolder, v,
                (TimelineItem.VideoItem) getItem(position).getEmbedItem());
          }
        }
      });
    } else if (viewHolder instanceof PhotoViewHolder) {
      // TODO Click to Photo
    }

    return viewHolder;
  }

  @Override public int getItemCount() {
    return ITEM_COUNT;
  }

  @Override public int firstVideoPosition() {
    int firstVideo = -1;
    for (int i = 0; i < ITEM_COUNT; i++) {
      if (TimelineItem.VideoItem.class.getSimpleName()
          .equals(getItem(i).getEmbedItem().getClassName())) {
        firstVideo = i;
        break;
      }
    }

    return firstVideo;
  }

  @Override public long getItemId(int position) {
    return super.getItemId(position);
  }

  @Override public int getItemViewType(int position) {
    String itemClassName = getItem(position).getEmbedItem().getClassName();
    return TimelineItem.VideoItem.class.getSimpleName().equals(itemClassName) ? TYPE_VIDEO
        : (TimelineItem.PhotoItem.class.getSimpleName().equals(itemClassName) ? TYPE_PHOTO
            : TYPE_OGP);
  }

  // Custom click event handling can be done neatly here.
  public static abstract class ItemClickListener implements OnItemClickListener {

    @Override
    public void onItemClick(RecyclerView.Adapter adapter, RecyclerView.ViewHolder viewHolder,
        View view, int adapterPosition, long itemId) {

    }

    protected abstract void onOgpItemClick(RecyclerView.ViewHolder viewHolder, View view,
        TimelineItem.OgpItem item);

    protected abstract void onPhotoClick(RecyclerView.ViewHolder viewHolder, View view,
        TimelineItem.PhotoItem item);

    protected abstract void onVideoClick(RecyclerView.ViewHolder viewHolder, View view,
        TimelineItem.VideoItem item);
  }

  // MediaPlayerManager implementation

  @Nullable @Override public ToroPlayer getPlayer() {
    return delegate.getPlayer();
  }

  @Override public void setPlayer(ToroPlayer player) {
    delegate.setPlayer(player);
  }

  @Override public void onRegistered() {
    delegate.onRegistered();
  }

  @Override public void onUnregistered() {
    delegate.onUnregistered();
  }

  @Override public void startPlayback() {
    delegate.startPlayback();
  }

  @Override public void pausePlayback() {
    delegate.pausePlayback();
  }

  @Override public void stopPlayback() {
    delegate.stopPlayback();
  }

  @Override public void saveVideoState(String videoId, @Nullable Long position, long duration) {
    delegate.saveVideoState(videoId, position, duration);
  }

  @Override public void restoreVideoState(String videoId) {
    delegate.restoreVideoState(videoId);
  }

  @Nullable @Override public Long getSavedPosition(String videoId) {
    return delegate.getSavedPosition(videoId);
  }
}

As you can see, there is not much a Custom MediaPlayerManager, I just call the delegation all the places. But it is required to implement a MediaPlayerManager to the Adapter here, so we can have the ability from both the Adapter, and the Manager.

7.5 Custom LayoutManager (to use with Toro)

Back to top

A cool new feature in Toro 2.1.0 is to support custom LayoutManager. The demo app contains CarouselLayoutManager sample to show you how you can use your own LayoutManager with Toro. Below is the implementation of that LayoutManager. In short, just implement the ToroLayoutManager interface and provide the correct information.

Least dependency

compile "im.ene.toro2:toro:2.1.0"
public class Advance1LayoutManager extends CarouselLayoutManager implements ToroLayoutManager {

  public Advance1LayoutManager(int orientation) {
    super(orientation);
  }

  public Advance1LayoutManager(int orientation, boolean circleLayout) {
    super(orientation, circleLayout);
  }

  @Override public int getFirstVisibleItemPosition() {
    return getCenterItemPosition();
  }

  @Override public int getLastVisibleItemPosition() {
    return getCenterItemPosition();
  }
}