blob: 25fddb69a8eee56ede56013d5362897efb27c8ab [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package android.support.v17.leanback.widget;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.support.v17.leanback.R;
import android.support.v4.view.ViewCompat;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.TextView;
import android.widget.ViewFlipper;
import java.util.ArrayList;
import java.util.List;
/**
* Abstract {@link Presenter} class for rendering media items in a playlist format.
* Media item data provided for this presenter can implement the interface
* {@link MultiActionsProvider}, if the media rows wish to contain custom actions.
* Media items in the playlist are arranged as a vertical list with each row holding each media
* item's details provided by the user of this class and a set of optional custom actions.
* Each media item's details and actions are separately focusable.
* The appearance of each one of the media row components can be controlled through setting
* theme's attributes.
* Each media item row provides a view flipper for switching between different views depending on
* the playback state.
* A default layout is provided by this presenter for rendering different playback states, or a
* custom layout can be provided by the user by overriding the
* playbackMediaItemNumberViewFlipperLayout attribute in the currently specified theme.
* Subclasses should also override {@link #getMediaPlayState(Object)} to provide the current play
* state of their media item model in case they wish to use different views depending on the
* playback state.
* The presenter can optionally provide line separators between media rows by setting
* {@link #setHasMediaRowSeparator(boolean)} to true.
* <p>
* Subclasses must override {@link #onBindMediaDetails} to implement their media item model
* data binding to each row view.
* </p>
* <p>
* The {@link OnItemViewClickedListener} and {@link OnItemViewSelectedListener}
* can be used in the same fashion to handle selection or click events on either of
* media details or each individual action views.
* </p>
* <p>
* {@link AbstractMediaListHeaderPresenter} can be used in conjunction with this presenter in
* order to display a playlist with a header view.
* </p>
*/
public abstract class AbstractMediaItemPresenter extends RowPresenter {
/**
* Different playback states of a media item
*/
/**
* Indicating that the media item is currently neither playing nor paused.
*/
public static final int PLAY_STATE_INITIAL = 0;
/**
* Indicating that the media item is currently paused.
*/
public static final int PLAY_STATE_PAUSED = 1;
/**
* Indicating that the media item is currently playing
*/
public static final int PLAY_STATE_PLAYING = 2;
final static Rect sTempRect = new Rect();
private int mBackgroundColor = Color.TRANSPARENT;
private boolean mBackgroundColorSet;
private boolean mMediaRowSeparator;
private int mThemeId;
private Presenter mMediaItemActionPresenter = new MediaItemActionPresenter();
/**
* Constructor used for creating an abstract media item presenter.
*/
public AbstractMediaItemPresenter() {
this(0);
}
/**
* Constructor used for creating an abstract media item presenter.
* @param themeId The resource id of the theme that defines attributes controlling the
* appearance of different widgets in a media item row.
*/
public AbstractMediaItemPresenter(int themeId) {
mThemeId = themeId;
setHeaderPresenter(null);
}
/**
* Sets the theme used to style a media item row components.
* @param themeId The resource id of the theme that defines attributes controlling the
* appearance of different widgets in a media item row.
*/
public void setThemeId(int themeId) {
mThemeId = themeId;
}
/**
* Return The resource id of the theme that defines attributes controlling the appearance of
* different widgets in a media item row.
*
* @return The resource id of the theme that defines attributes controlling the appearance of
* different widgets in a media item row.
*/
public int getThemeId() {
return mThemeId;
}
/**
* Sets the action presenter rendering each optional custom action within each media item row.
* @param actionPresenter the presenter to be used for rendering a media item row actions.
*/
public void setActionPresenter(Presenter actionPresenter) {
mMediaItemActionPresenter = actionPresenter;
}
/**
* Return the presenter used to render a media item row actions.
*
* @return the presenter used to render a media item row actions.
*/
public Presenter getActionPresenter() {
return mMediaItemActionPresenter;
}
/**
* The ViewHolder for the {@link AbstractMediaItemPresenter}. It references different views
* that place different meta-data corresponding to a media item details, actions, selector,
* listeners, and presenters,
*/
public static class ViewHolder extends RowPresenter.ViewHolder {
private final View mMediaRowView;
private final View mSelectorView;
private final View mMediaItemDetailsView;
private final ViewFlipper mMediaItemNumberViewFlipper;
private final TextView mMediaItemNumberView;
private final View mMediaItemPausedView;
private final View mMediaItemPlayingView;
private final TextView mMediaItemNameView;
private final TextView mMediaItemDurationView;
private final View mMediaItemRowSeparator;
private final ViewGroup mMediaItemActionsContainer;
private final List<Presenter.ViewHolder> mActionViewHolders;
private MultiActionsProvider.MultiAction[] mMediaItemRowActions;
AbstractMediaItemPresenter mRowPresenter;
private ValueAnimator mFocusViewAnimator;
public ViewHolder(View view) {
super(view);
mSelectorView = view.findViewById(R.id.mediaRowSelector);
mMediaRowView = view.findViewById(R.id.mediaItemRow);
mMediaItemDetailsView = view.findViewById(R.id.mediaItemDetails);
mMediaItemNameView = (TextView) view.findViewById(R.id.mediaItemName);
mMediaItemDurationView = (TextView) view.findViewById(R.id.mediaItemDuration);
mMediaItemRowSeparator = view.findViewById(R.id.mediaRowSeparator);
mMediaItemActionsContainer = (ViewGroup) view.findViewById(
R.id.mediaItemActionsContainer);
mActionViewHolders = new ArrayList<Presenter.ViewHolder>();
getMediaItemDetailsView().setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view) {
if (getOnItemViewClickedListener() != null) {
getOnItemViewClickedListener().onItemClicked(null, null,
ViewHolder.this, getRowObject());
}
}
});
getMediaItemDetailsView().setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
mFocusViewAnimator = updateSelector(mSelectorView, view, mFocusViewAnimator,
true);
}
});
mMediaItemNumberViewFlipper =
(ViewFlipper) view.findViewById(R.id.mediaItemNumberViewFlipper);
TypedValue typedValue = new TypedValue();
boolean found = view.getContext().getTheme().resolveAttribute(
R.attr.playbackMediaItemNumberViewFlipperLayout, typedValue, true);
View mergeView = LayoutInflater.from(view.getContext()).
inflate(found ? typedValue.resourceId :
R.layout.lb_media_item_number_view_flipper,
mMediaItemNumberViewFlipper, true);
mMediaItemNumberView = (TextView) mergeView.findViewById(R.id.initial);
mMediaItemPausedView = mergeView.findViewById(R.id.paused);
mMediaItemPlayingView = mergeView.findViewById(R.id.playing);
}
/**
* Binds the actions in a media item row object to their views. This consists of creating
* (or reusing the existing) action view holders, and populating them with the actions'
* icons.
*/
public void onBindRowActions() {
for (int i = getMediaItemActionsContainer().getChildCount() - 1;
i >= mActionViewHolders.size(); i--) {
getMediaItemActionsContainer().removeViewAt(i);
mActionViewHolders.remove(i);
}
mMediaItemRowActions = null;
Object rowObject = getRowObject();
final MultiActionsProvider.MultiAction[] actionList;
if (rowObject instanceof MultiActionsProvider) {
actionList = ((MultiActionsProvider) rowObject).getActions();
} else {
return;
}
Presenter actionPresenter = mRowPresenter.getActionPresenter();
if (actionPresenter == null) {
return;
}
mMediaItemRowActions = actionList;
for (int i = mActionViewHolders.size(); i < actionList.length; i++) {
final int actionIndex = i;
final Presenter.ViewHolder actionViewHolder = actionPresenter.
onCreateViewHolder(getMediaItemActionsContainer());
getMediaItemActionsContainer().addView(actionViewHolder.view);
mActionViewHolders.add(actionViewHolder);
actionViewHolder.view.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
mFocusViewAnimator = updateSelector(mSelectorView, view,
mFocusViewAnimator, false);
}
});
actionViewHolder.view.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
if (getOnItemViewClickedListener() != null) {
getOnItemViewClickedListener().onItemClicked(
actionViewHolder, mMediaItemRowActions[actionIndex],
ViewHolder.this, getRowObject());
}
}
});
}
if (mMediaItemActionsContainer != null) {
for (int i = 0; i < actionList.length; i++) {
Presenter.ViewHolder avh = mActionViewHolders.get(i);
actionPresenter.onUnbindViewHolder(avh);
actionPresenter.onBindViewHolder(avh, mMediaItemRowActions[i]);
}
}
}
int findActionIndex(MultiActionsProvider.MultiAction action) {
if (mMediaItemRowActions != null) {
for (int i = 0; i < mMediaItemRowActions.length; i++) {
if (mMediaItemRowActions[i] == action) {
return i;
}
}
}
return -1;
}
/**
* Notifies an action has changed in this media row and the UI needs to be updated
* @param action The action whose state has changed
*/
public void notifyActionChanged(MultiActionsProvider.MultiAction action) {
Presenter actionPresenter = mRowPresenter.getActionPresenter();
if (actionPresenter == null) {
return;
}
int actionIndex = findActionIndex(action);
if (actionIndex >= 0) {
Presenter.ViewHolder actionViewHolder = mActionViewHolders.get(actionIndex);
actionPresenter.onUnbindViewHolder(actionViewHolder);
actionPresenter.onBindViewHolder(actionViewHolder, action);
}
}
/**
* Notifies the content of the media item details in a row has changed and triggers updating
* the UI. This causes {@link #onBindMediaDetails(ViewHolder, Object)}
* on the user's provided presenter to be called back, allowing them to update UI
* accordingly.
*/
public void notifyDetailsChanged() {
mRowPresenter.onUnbindMediaDetails(this);
mRowPresenter.onBindMediaDetails(this, getRowObject());
}
/**
* Notifies the playback state of the media item row has changed. This in turn triggers
* updating of the UI for that media item row if corresponding views are specified for each
* playback state.
* By default, 3 views are provided for each playback state, or these views can be provided
* by the user.
*/
public void notifyPlayStateChanged() {
mRowPresenter.onBindMediaPlayState(this);
}
/**
* @return The SelectorView responsible for highlighting the in-focus view within each
* media item row
*/
public View getSelectorView() {
return mSelectorView;
}
/**
* @return The FlipperView responsible for flipping between different media item number
* views depending on the playback state
*/
public ViewFlipper getMediaItemNumberViewFlipper() {
return mMediaItemNumberViewFlipper;
}
/**
* @return The TextView responsible for rendering the media item number.
* This view is rendered when the media item row is neither playing nor paused.
*/
public TextView getMediaItemNumberView() {
return mMediaItemNumberView;
}
/**
* @return The view rendered when the media item row is paused.
*/
public View getMediaItemPausedView() {
return mMediaItemPausedView;
}
/**
* @return The view rendered when the media item row is playing.
*/
public View getMediaItemPlayingView() {
return mMediaItemPlayingView;
}
/**
* Flips to the view at index 'position'. This position corresponds to the index of a
* particular view within the ViewFlipper layout specified for the MediaItemNumberView
* (see playbackMediaItemNumberViewFlipperLayout attribute).
* @param position The index of the child view to display.
*/
public void setSelectedMediaItemNumberView(int position) {
if (position >= 0 & position < mMediaItemNumberViewFlipper.getChildCount()) {
mMediaItemNumberViewFlipper.setDisplayedChild(position);
}
}
/**
* Returns the view displayed when the media item is neither playing nor paused,
* corresponding to the playback state of PLAY_STATE_INITIAL.
* @return The TextView responsible for rendering the media item name.
*/
public TextView getMediaItemNameView() {
return mMediaItemNameView;
}
/**
* @return The TextView responsible for rendering the media item duration
*/
public TextView getMediaItemDurationView() {
return mMediaItemDurationView;
}
/**
* @return The view container of media item details
*/
public View getMediaItemDetailsView() {
return mMediaItemDetailsView;
}
/**
* @return The view responsible for rendering the separator line between media rows
*/
public View getMediaItemRowSeparator() {
return mMediaItemRowSeparator;
}
/**
* @return The view containing the set of custom actions
*/
public ViewGroup getMediaItemActionsContainer() {
return mMediaItemActionsContainer;
}
/**
* @return Array of MultiActions displayed for this media item row
*/
public MultiActionsProvider.MultiAction[] getMediaItemRowActions() {
return mMediaItemRowActions;
}
}
@Override
protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
Context context = parent.getContext();
if (mThemeId != 0) {
context = new ContextThemeWrapper(context, mThemeId);
}
View view = LayoutInflater.from(context).
inflate(R.layout.lb_row_media_item, parent, false);
final ViewHolder vh = new ViewHolder(view);
vh.mRowPresenter = this;
if (mBackgroundColorSet) {
vh.mMediaRowView.setBackgroundColor(mBackgroundColor);
}
return vh;
}
@Override
public boolean isUsingDefaultSelectEffect() {
return false;
}
@Override
protected boolean isClippingChildren() {
return true;
}
@Override
protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
super.onBindRowViewHolder(vh, item);
final ViewHolder mvh = (ViewHolder) vh;
onBindRowActions(mvh);
mvh.getMediaItemRowSeparator().setVisibility(hasMediaRowSeparator() ? View.VISIBLE :
View.GONE);
onBindMediaPlayState(mvh);
onBindMediaDetails((ViewHolder) vh, item);
}
/**
* Binds the given media item object action to the given ViewHolder's action views.
* @param vh ViewHolder for the media item.
*/
protected void onBindRowActions(ViewHolder vh) {
vh.onBindRowActions();
}
/**
* Sets the background color for the row views within the playlist.
* If this is not set, a default color, defaultBrandColor, from theme is used.
* This defaultBrandColor defaults to android:attr/colorPrimary on v21, if it's specified.
* @param color The ARGB color used to set as the media list background color.
*/
public void setBackgroundColor(int color) {
mBackgroundColorSet = true;
mBackgroundColor = color;
}
/**
* Specifies whether a line separator should be used between media item rows.
* @param hasSeparator true if a separator should be displayed, false otherwise.
*/
public void setHasMediaRowSeparator(boolean hasSeparator) {
mMediaRowSeparator = hasSeparator;
}
public boolean hasMediaRowSeparator() {
return mMediaRowSeparator;
}
/**
* Binds the media item details to their views provided by the
* {@link AbstractMediaItemPresenter}.
* This method is to be overridden by the users of this presenter.
* The subclasses of this presenter can access and bind individual views for either of the
* media item number, name, or duration (depending on whichever views are visible according to
* the providing theme attributes), by calling {@link ViewHolder#getMediaItemNumberView()},
* {@link ViewHolder#getMediaItemNameView()}, and {@link ViewHolder#getMediaItemDurationView()},
* on the {@link ViewHolder} provided as the argument {@code vh} of this presenter.
*
* @param vh The ViewHolder for this {@link AbstractMediaItemPresenter}.
* @param item The media item row object being presented.
*/
protected abstract void onBindMediaDetails(ViewHolder vh, Object item);
/**
* Unbinds the media item details from their views provided by the
* {@link AbstractMediaItemPresenter}.
* This method can be overridden by the subclasses of this presenter if required.
* @param vh ViewHolder to unbind from.
*/
protected void onUnbindMediaDetails(ViewHolder vh) {
}
/**
* Binds the media item number view to the appropriate play state view of the media item.
* The play state of the media item is extracted by calling {@link #getMediaPlayState(Object)} for
* the media item embedded within this view.
* This method triggers updating of the playback state UI if corresponding views are specified
* for the current playback state.
* By default, 3 views are provided for each playback state, or these views can be provided
* by the user.
*/
public void onBindMediaPlayState(ViewHolder vh) {
int childIndex = calculateMediaItemNumberFlipperIndex(vh);
if (childIndex != -1 && vh.mMediaItemNumberViewFlipper.getDisplayedChild() != childIndex) {
vh.mMediaItemNumberViewFlipper.setDisplayedChild(childIndex);
}
}
static int calculateMediaItemNumberFlipperIndex(ViewHolder vh) {
int childIndex = -1;
int newPlayState = vh.mRowPresenter.getMediaPlayState(vh.getRowObject());
switch (newPlayState) {
case PLAY_STATE_INITIAL:
childIndex = (vh.mMediaItemNumberView == null) ? -1 :
vh.mMediaItemNumberViewFlipper.indexOfChild(vh.mMediaItemNumberView);
break;
case PLAY_STATE_PAUSED:
childIndex = (vh.mMediaItemPausedView == null) ? -1 :
vh.mMediaItemNumberViewFlipper.indexOfChild(vh.mMediaItemPausedView);
break;
case PLAY_STATE_PLAYING:
childIndex = (vh.mMediaItemPlayingView == null) ? -1 :
vh.mMediaItemNumberViewFlipper.indexOfChild(vh.mMediaItemPlayingView);
}
return childIndex;
}
/**
* Called when the given ViewHolder wants to unbind the play state view.
* @param vh The ViewHolder to unbind from.
*/
public void onUnbindMediaPlayState(ViewHolder vh) {
}
/**
* Returns the current play state of the given media item. By default, this method returns
* PLAY_STATE_INITIAL which causes the media item number
* {@link ViewHolder#getMediaItemNameView()} to be displayed for different
* playback states. Users of this class should override this method in order to provide the
* play state of their custom media item data model.
* @param item The media item
* @return The current play state of this media item
*/
protected int getMediaPlayState(Object item) {
return PLAY_STATE_INITIAL;
}
/**
* Each media item row can have multiple focusable elements; the details on the left and a set
* of optional custom actions on the right.
* The selector is a highlight that moves to highlight to cover whichever views is in focus.
*
* @param selectorView the selector view used to highlight an individual element within a row.
* @param focusChangedView The component within the media row whose focus got changed.
* @param layoutAnimator the ValueAnimator producing animation frames for the selector's width
* and x-translation, generated by this method and stored for the each
* {@link ViewHolder}.
* @param isDetails Whether the changed-focused view is for a media item details (true) or
* an action (false).
*/
private static ValueAnimator updateSelector(final View selectorView,
View focusChangedView, ValueAnimator layoutAnimator, boolean isDetails) {
int animationDuration = focusChangedView.getContext().getResources()
.getInteger(android.R.integer.config_shortAnimTime);
DecelerateInterpolator interpolator = new DecelerateInterpolator();
int layoutDirection = ViewCompat.getLayoutDirection(selectorView);
if (!focusChangedView.hasFocus()) {
// if neither of the details or action views are in focus (ie. another row is in focus),
// animate the selector out.
selectorView.animate().cancel();
selectorView.animate().alpha(0f).setDuration(animationDuration)
.setInterpolator(interpolator).start();
// keep existing layout animator
return layoutAnimator;
} else {
// cancel existing layout animator
if (layoutAnimator != null) {
layoutAnimator.cancel();
layoutAnimator = null;
}
float currentAlpha = selectorView.getAlpha();
selectorView.animate().alpha(1f).setDuration(animationDuration)
.setInterpolator(interpolator).start();
final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams)
selectorView.getLayoutParams();
ViewGroup rootView = (ViewGroup) selectorView.getParent();
sTempRect.set(0, 0, focusChangedView.getWidth(), focusChangedView.getHeight());
rootView.offsetDescendantRectToMyCoords(focusChangedView, sTempRect);
if (isDetails) {
if (layoutDirection == View.LAYOUT_DIRECTION_RTL ) {
sTempRect.right += rootView.getHeight();
sTempRect.left -= rootView.getHeight() / 2;
} else {
sTempRect.left -= rootView.getHeight();
sTempRect.right += rootView.getHeight() / 2;
}
}
final int targetLeft = sTempRect.left;
final int targetWidth = sTempRect.width();
final float deltaWidth = lp.width - targetWidth;
final float deltaLeft = lp.leftMargin - targetLeft;
if (deltaLeft == 0f && deltaWidth == 0f)
{
// no change needed
} else if (currentAlpha == 0f) {
// change selector to the proper width and marginLeft without animation.
lp.width = targetWidth;
lp.leftMargin = targetLeft;
selectorView.requestLayout();
} else {
// animate the selector to the proper width and marginLeft.
layoutAnimator = ValueAnimator.ofFloat(0f, 1f);
layoutAnimator.setDuration(animationDuration);
layoutAnimator.setInterpolator(interpolator);
layoutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// Set width to the proper width for this animation step.
float fractionToEnd = 1f - valueAnimator.getAnimatedFraction();
lp.leftMargin = Math.round(targetLeft + deltaLeft * fractionToEnd);
lp.width = Math.round(targetWidth + deltaWidth * fractionToEnd);
selectorView.requestLayout();
}
});
layoutAnimator.start();
}
return layoutAnimator;
}
}
}