/* * Copyright (C) 2014 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.support.v17.leanback.R; import android.util.TypedValue; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.view.KeyEvent; /** * A {@link Row} of playback controls to be displayed by a {@link PlaybackControlsRowPresenter}. * * This row consists of some optional item detail, a series of primary actions, * and an optional series of secondary actions. * *

* Controls are specified via an {@link ObjectAdapter} containing one or more * {@link Action}s. *

*

* Adapters should have their {@link PresenterSelector} set to an instance of * {@link ControlButtonPresenterSelector}. *

*/ public class PlaybackControlsRow extends Row { /** * Base class for an action comprised of a series of icons. */ public static abstract class MultiAction extends Action { private int mIndex; private Drawable[] mDrawables; private String[] mLabels; private String[] mLabels2; /** * Constructor * @param id The id of the Action. */ public MultiAction(int id) { super(id); } /** * Sets the array of drawables. The size of the array defines the range * of valid indices for this action. */ public void setDrawables(Drawable[] drawables) { mDrawables = drawables; setIndex(0); } /** * Sets the array of strings used as labels. The size of the array defines the range * of valid indices for this action. The labels are used to define the accessibility * content description unless secondary labels are provided. */ public void setLabels(String[] labels) { mLabels = labels; setIndex(0); } /** * Sets the array of strings used as secondary labels. These labels are used * in place of the primary labels for accessibility content description only. */ public void setSecondaryLabels(String[] labels) { mLabels2 = labels; setIndex(0); } /** * Returns the number of actions. */ public int getActionCount() { if (mDrawables != null) { return mDrawables.length; } if (mLabels != null) { return mLabels.length; } return 0; } /** * Returns the drawable at the given index. */ public Drawable getDrawable(int index) { return mDrawables == null ? null : mDrawables[index]; } /** * Returns the label at the given index. */ public String getLabel(int index) { return mLabels == null ? null : mLabels[index]; } /** * Returns the secondary label at the given index. */ public String getSecondaryLabel(int index) { return mLabels2 == null ? null : mLabels2[index]; } /** * Increments the index, wrapping to zero once the end is reached. */ public void nextIndex() { setIndex(mIndex < getActionCount() - 1 ? mIndex + 1 : 0); } /** * Sets the current index. */ public void setIndex(int index) { mIndex = index; if (mDrawables != null) { setIcon(mDrawables[mIndex]); } if (mLabels != null) { setLabel1(mLabels[mIndex]); } if (mLabels2 != null) { setLabel2(mLabels2[mIndex]); } } /** * Returns the current index. */ public int getIndex() { return mIndex; } } /** * An action displaying icons for play and pause. */ public static class PlayPauseAction extends MultiAction { /** * Action index for the play icon. */ public static int PLAY = 0; /** * Action index for the pause icon. */ public static int PAUSE = 1; /** * Constructor * @param context Context used for loading resources. */ public PlayPauseAction(Context context) { super(R.id.lb_control_play_pause); Drawable[] drawables = new Drawable[2]; drawables[PLAY] = getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_play); drawables[PAUSE] = getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_pause); setDrawables(drawables); String[] labels = new String[drawables.length]; labels[PLAY] = context.getString(R.string.lb_playback_controls_play); labels[PAUSE] = context.getString(R.string.lb_playback_controls_pause); setLabels(labels); addKeyCode(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE); addKeyCode(KeyEvent.KEYCODE_MEDIA_PLAY); addKeyCode(KeyEvent.KEYCODE_MEDIA_PAUSE); } } /** * An action displaying an icon for fast forward. */ public static class FastForwardAction extends MultiAction { /** * Constructor * @param context Context used for loading resources. */ public FastForwardAction(Context context) { this(context, 1); } /** * Constructor * @param context Context used for loading resources. * @param numSpeeds Number of supported fast forward speeds. */ public FastForwardAction(Context context, int numSpeeds) { super(R.id.lb_control_fast_forward); if (numSpeeds < 1) { throw new IllegalArgumentException("numSpeeds must be > 0"); } Drawable[] drawables = new Drawable[numSpeeds + 1]; drawables[0] = getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_fast_forward); setDrawables(drawables); String[] labels = new String[getActionCount()]; labels[0] = context.getString(R.string.lb_playback_controls_fast_forward); String[] labels2 = new String[getActionCount()]; labels2[0] = labels[0]; for (int i = 1; i <= numSpeeds; i++) { int multiplier = i + 1; labels[i] = context.getResources().getString( R.string.lb_control_display_fast_forward_multiplier, multiplier); labels2[i] = context.getResources().getString( R.string.lb_playback_controls_fast_forward_multiplier, multiplier); } setLabels(labels); setSecondaryLabels(labels2); addKeyCode(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD); } } /** * An action displaying an icon for rewind. */ public static class RewindAction extends MultiAction { /** * Constructor * @param context Context used for loading resources. */ public RewindAction(Context context) { this(context, 1); } /** * Constructor * @param context Context used for loading resources. * @param numSpeeds Number of supported fast forward speeds. */ public RewindAction(Context context, int numSpeeds) { super(R.id.lb_control_fast_rewind); if (numSpeeds < 1) { throw new IllegalArgumentException("numSpeeds must be > 0"); } Drawable[] drawables = new Drawable[numSpeeds + 1]; drawables[0] = getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_rewind); setDrawables(drawables); String[] labels = new String[getActionCount()]; labels[0] = context.getString(R.string.lb_playback_controls_rewind); String[] labels2 = new String[getActionCount()]; labels2[0] = labels[0]; for (int i = 1; i <= numSpeeds; i++) { int multiplier = i + 1; labels[i] = labels[i] = context.getResources().getString( R.string.lb_control_display_rewind_multiplier, multiplier); labels2[i] = context.getResources().getString( R.string.lb_playback_controls_rewind_multiplier, multiplier); } setLabels(labels); setSecondaryLabels(labels2); addKeyCode(KeyEvent.KEYCODE_MEDIA_REWIND); } } /** * An action displaying an icon for skip next. */ public static class SkipNextAction extends Action { /** * Constructor * @param context Context used for loading resources. */ public SkipNextAction(Context context) { super(R.id.lb_control_skip_next); setIcon(getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_skip_next)); setLabel1(context.getString(R.string.lb_playback_controls_skip_next)); addKeyCode(KeyEvent.KEYCODE_MEDIA_NEXT); } } /** * An action displaying an icon for skip previous. */ public static class SkipPreviousAction extends Action { /** * Constructor * @param context Context used for loading resources. */ public SkipPreviousAction(Context context) { super(R.id.lb_control_skip_previous); setIcon(getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_skip_previous)); setLabel1(context.getString(R.string.lb_playback_controls_skip_previous)); addKeyCode(KeyEvent.KEYCODE_MEDIA_PREVIOUS); } } /** * An action displaying an icon for picture-in-picture. */ public static class PictureInPictureAction extends Action { /** * Constructor * @param context Context used for loading resources. */ public PictureInPictureAction(Context context) { super(R.id.lb_control_picture_in_picture); setIcon(getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_picture_in_picture)); setLabel1(context.getString(R.string.lb_playback_controls_picture_in_picture)); addKeyCode(KeyEvent.KEYCODE_WINDOW); } } /** * An action displaying an icon for "more actions". */ public static class MoreActions extends Action { /** * Constructor * @param context Context used for loading resources. */ public MoreActions(Context context) { super(R.id.lb_control_more_actions); setIcon(context.getResources().getDrawable(R.drawable.lb_ic_more)); setLabel1(context.getString(R.string.lb_playback_controls_more_actions)); } } /** * A base class for displaying a thumbs action. */ public static abstract class ThumbsAction extends MultiAction { /** * Action index for the solid thumb icon. */ public static int SOLID = 0; /** * Action index for the outline thumb icon. */ public static int OUTLINE = 1; /** * Constructor * @param context Context used for loading resources. */ public ThumbsAction(int id, Context context, int solidIconIndex, int outlineIconIndex) { super(id); Drawable[] drawables = new Drawable[2]; drawables[SOLID] = getStyledDrawable(context, solidIconIndex); drawables[OUTLINE] = getStyledDrawable(context, outlineIconIndex); setDrawables(drawables); } } /** * An action displaying an icon for thumbs up. */ public static class ThumbsUpAction extends ThumbsAction { public ThumbsUpAction(Context context) { super(R.id.lb_control_thumbs_up, context, R.styleable.lbPlaybackControlsActionIcons_thumb_up, R.styleable.lbPlaybackControlsActionIcons_thumb_up_outline); String[] labels = new String[getActionCount()]; labels[SOLID] = context.getString(R.string.lb_playback_controls_thumb_up); labels[OUTLINE] = context.getString(R.string.lb_playback_controls_thumb_up_outline); setLabels(labels); } } /** * An action displaying an icon for thumbs down. */ public static class ThumbsDownAction extends ThumbsAction { public ThumbsDownAction(Context context) { super(R.id.lb_control_thumbs_down, context, R.styleable.lbPlaybackControlsActionIcons_thumb_down, R.styleable.lbPlaybackControlsActionIcons_thumb_down_outline); String[] labels = new String[getActionCount()]; labels[SOLID] = context.getString(R.string.lb_playback_controls_thumb_down); labels[OUTLINE] = context.getString(R.string.lb_playback_controls_thumb_down_outline); setLabels(labels); } } /** * An action for displaying three repeat states: none, one, or all. */ public static class RepeatAction extends MultiAction { /** * Action index for the repeat-none icon. */ public static int NONE = 0; /** * Action index for the repeat-all icon. */ public static int ALL = 1; /** * Action index for the repeat-one icon. */ public static int ONE = 2; /** * Constructor * @param context Context used for loading resources. */ public RepeatAction(Context context) { this(context, getIconHighlightColor(context)); } /** * Constructor * @param context Context used for loading resources * @param highlightColor Color to display the repeat-all and repeat0one icons. */ public RepeatAction(Context context, int highlightColor) { this(context, highlightColor, highlightColor); } /** * Constructor * @param context Context used for loading resources * @param repeatAllColor Color to display the repeat-all icon. * @param repeatOneColor Color to display the repeat-one icon. */ public RepeatAction(Context context, int repeatAllColor, int repeatOneColor) { super(R.id.lb_control_repeat); Drawable[] drawables = new Drawable[3]; BitmapDrawable repeatDrawable = (BitmapDrawable) getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_repeat); BitmapDrawable repeatOneDrawable = (BitmapDrawable) getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_repeat_one); drawables[NONE] = repeatDrawable; drawables[ALL] = repeatDrawable == null ? null : new BitmapDrawable(context.getResources(), createBitmap(repeatDrawable.getBitmap(), repeatAllColor)); drawables[ONE] = repeatOneDrawable == null ? null : new BitmapDrawable(context.getResources(), createBitmap(repeatOneDrawable.getBitmap(), repeatOneColor)); setDrawables(drawables); String[] labels = new String[drawables.length]; // Note, labels denote the action taken when clicked labels[NONE] = context.getString(R.string.lb_playback_controls_repeat_all); labels[ALL] = context.getString(R.string.lb_playback_controls_repeat_one); labels[ONE] = context.getString(R.string.lb_playback_controls_repeat_none); setLabels(labels); } } /** * An action for displaying a shuffle icon. */ public static class ShuffleAction extends MultiAction { public static int OFF = 0; public static int ON = 1; /** * Constructor * @param context Context used for loading resources. */ public ShuffleAction(Context context) { this(context, getIconHighlightColor(context)); } /** * Constructor * @param context Context used for loading resources. * @param highlightColor Color for the highlighted icon state. */ public ShuffleAction(Context context, int highlightColor) { super(R.id.lb_control_shuffle); BitmapDrawable uncoloredDrawable = (BitmapDrawable) getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_shuffle); Drawable[] drawables = new Drawable[2]; drawables[OFF] = uncoloredDrawable; drawables[ON] = new BitmapDrawable(context.getResources(), createBitmap(uncoloredDrawable.getBitmap(), highlightColor)); setDrawables(drawables); String[] labels = new String[drawables.length]; labels[OFF] = context.getString(R.string.lb_playback_controls_shuffle_enable); labels[ON] = context.getString(R.string.lb_playback_controls_shuffle_disable); setLabels(labels); } } /** * An action for displaying a HQ (High Quality) icon. */ public static class HighQualityAction extends MultiAction { public static int OFF = 0; public static int ON = 1; /** * Constructor * @param context Context used for loading resources. */ public HighQualityAction(Context context) { this(context, getIconHighlightColor(context)); } /** * Constructor * @param context Context used for loading resources. * @param highlightColor Color for the highlighted icon state. */ public HighQualityAction(Context context, int highlightColor) { super(R.id.lb_control_high_quality); BitmapDrawable uncoloredDrawable = (BitmapDrawable) getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_high_quality); Drawable[] drawables = new Drawable[2]; drawables[OFF] = uncoloredDrawable; drawables[ON] = new BitmapDrawable(context.getResources(), createBitmap(uncoloredDrawable.getBitmap(), highlightColor)); setDrawables(drawables); String[] labels = new String[drawables.length]; labels[OFF] = context.getString(R.string.lb_playback_controls_high_quality_enable); labels[ON] = context.getString(R.string.lb_playback_controls_high_quality_disable); setLabels(labels); } } /** * An action for displaying a CC (Closed Captioning) icon. */ public static class ClosedCaptioningAction extends MultiAction { public static int OFF = 0; public static int ON = 1; /** * Constructor * @param context Context used for loading resources. */ public ClosedCaptioningAction(Context context) { this(context, getIconHighlightColor(context)); } /** * Constructor * @param context Context used for loading resources. * @param highlightColor Color for the highlighted icon state. */ public ClosedCaptioningAction(Context context, int highlightColor) { super(R.id.lb_control_closed_captioning); BitmapDrawable uncoloredDrawable = (BitmapDrawable) getStyledDrawable(context, R.styleable.lbPlaybackControlsActionIcons_closed_captioning); Drawable[] drawables = new Drawable[2]; drawables[OFF] = uncoloredDrawable; drawables[ON] = new BitmapDrawable(context.getResources(), createBitmap(uncoloredDrawable.getBitmap(), highlightColor)); setDrawables(drawables); String[] labels = new String[drawables.length]; labels[OFF] = context.getString(R.string.lb_playback_controls_closed_captioning_enable); labels[ON] = context.getString(R.string.lb_playback_controls_closed_captioning_disable); setLabels(labels); } } static Bitmap createBitmap(Bitmap bitmap, int color) { Bitmap dst = bitmap.copy(bitmap.getConfig(), true); Canvas canvas = new Canvas(dst); Paint paint = new Paint(); paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); canvas.drawBitmap(bitmap, 0, 0, paint); return dst; } static int getIconHighlightColor(Context context) { TypedValue outValue = new TypedValue(); if (context.getTheme().resolveAttribute(R.attr.playbackControlsIconHighlightColor, outValue, true)) { return outValue.data; } return context.getResources().getColor(R.color.lb_playback_icon_highlight_no_theme); } static Drawable getStyledDrawable(Context context, int index) { TypedValue outValue = new TypedValue(); if (!context.getTheme().resolveAttribute( R.attr.playbackControlsActionIcons, outValue, false)) { return null; } TypedArray array = context.getTheme().obtainStyledAttributes(outValue.data, R.styleable.lbPlaybackControlsActionIcons); Drawable drawable = array.getDrawable(index); array.recycle(); return drawable; } private Object mItem; private Drawable mImageDrawable; private ObjectAdapter mPrimaryActionsAdapter; private ObjectAdapter mSecondaryActionsAdapter; private int mTotalTimeMs; private int mCurrentTimeMs; private int mBufferedProgressMs; private OnPlaybackStateChangedListener mListener; /** * Constructor for a PlaybackControlsRow that displays some details from * the given item. * * @param item The main item for the row. */ public PlaybackControlsRow(Object item) { mItem = item; } /** * Constructor for a PlaybackControlsRow that has no item details. */ public PlaybackControlsRow() { } /** * Returns the main item for the details page. */ public final Object getItem() { return mItem; } /** * Sets a {link @Drawable} image for this row. *

If set after the row has been bound to a view, the adapter must be notified that * this row has changed.

* * @param drawable The drawable to set. */ public final void setImageDrawable(Drawable drawable) { mImageDrawable = drawable; } /** * Sets a {@link Bitmap} for this row. *

If set after the row has been bound to a view, the adapter must be notified that * this row has changed.

* * @param context The context to retrieve display metrics from. * @param bm The bitmap to set. */ public final void setImageBitmap(Context context, Bitmap bm) { mImageDrawable = new BitmapDrawable(context.getResources(), bm); } /** * Returns the image {@link Drawable} of this row. * * @return The overview's image drawable, or null if no drawable has been * assigned. */ public final Drawable getImageDrawable() { return mImageDrawable; } /** * Sets the primary actions {@link ObjectAdapter}. *

If set after the row has been bound to a view, the adapter must be notified that * this row has changed.

*/ public final void setPrimaryActionsAdapter(ObjectAdapter adapter) { mPrimaryActionsAdapter = adapter; } /** * Sets the secondary actions {@link ObjectAdapter}. *

If set after the row has been bound to a view, the adapter must be notified that * this row has changed.

*/ public final void setSecondaryActionsAdapter(ObjectAdapter adapter) { mSecondaryActionsAdapter = adapter; } /** * Returns the primary actions {@link ObjectAdapter}. */ public final ObjectAdapter getPrimaryActionsAdapter() { return mPrimaryActionsAdapter; } /** * Returns the secondary actions {@link ObjectAdapter}. */ public final ObjectAdapter getSecondaryActionsAdapter() { return mSecondaryActionsAdapter; } /** * Sets the total time in milliseconds for the playback controls row. *

If set after the row has been bound to a view, the adapter must be notified that * this row has changed.

*/ public void setTotalTime(int ms) { mTotalTimeMs = ms; } /** * Returns the total time in milliseconds for the playback controls row. */ public int getTotalTime() { return mTotalTimeMs; } /** * Sets the current time in milliseconds for the playback controls row. * If this row is bound to a view, the view will automatically * be updated to reflect the new value. */ public void setCurrentTime(int ms) { if (mCurrentTimeMs != ms) { mCurrentTimeMs = ms; currentTimeChanged(); } } /** * Returns the current time in milliseconds for the playback controls row. */ public int getCurrentTime() { return mCurrentTimeMs; } /** * Sets the buffered progress for the playback controls row. * If this row is bound to a view, the view will automatically * be updated to reflect the new value. */ public void setBufferedProgress(int ms) { if (mBufferedProgressMs != ms) { mBufferedProgressMs = ms; bufferedProgressChanged(); } } /** * Returns the buffered progress for the playback controls row. */ public int getBufferedProgress() { return mBufferedProgressMs; } /** * Returns the Action associated with the given keycode, or null if no associated action exists. * Searches the primary adapter first, then the secondary adapter. */ public Action getActionForKeyCode(int keyCode) { Action action = getActionForKeyCode(getPrimaryActionsAdapter(), keyCode); if (action != null) { return action; } return getActionForKeyCode(getSecondaryActionsAdapter(), keyCode); } /** * Returns the Action associated with the given keycode, or null if no associated action exists. */ public Action getActionForKeyCode(ObjectAdapter adapter, int keyCode) { if (adapter != mPrimaryActionsAdapter && adapter != mSecondaryActionsAdapter) { throw new IllegalArgumentException("Invalid adapter"); } for (int i = 0; i < adapter.size(); i++) { Action action = (Action) adapter.get(i); if (action.respondsToKeyCode(keyCode)) { return action; } } return null; } interface OnPlaybackStateChangedListener { public void onCurrentTimeChanged(int currentTimeMs); public void onBufferedProgressChanged(int bufferedProgressMs); } /** * Sets a listener to be called when the playback state changes. */ void setOnPlaybackStateChangedListener(OnPlaybackStateChangedListener listener) { mListener = listener; } /** * Returns the playback state listener. */ OnPlaybackStateChangedListener getOnPlaybackStateChangedListener() { return mListener; } private void currentTimeChanged() { if (mListener != null) { mListener.onCurrentTimeChanged(mCurrentTimeMs); } } private void bufferedProgressChanged() { if (mListener != null) { mListener.onBufferedProgressChanged(mBufferedProgressMs); } } }