blob: 34c20046e3bb42e33b605b85dbc7a130df56da7d [file] [log] [blame]
/*
* 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.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.os.Handler;
import android.support.annotation.ColorInt;
import android.support.v17.leanback.R;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import java.util.Collection;
/**
* Renders a {@link DetailsOverviewRow} to display an overview of an item.
* Typically this row will be the first row in a fragment
* such as the {@link android.support.v17.leanback.app.DetailsFragment
* DetailsFragment}. The View created by the DetailsOverviewRowPresenter is made in three parts:
* ImageView on the left, action list view on the bottom and a customizable detailed
* description view on the right.
*
* <p>The detailed description is rendered using a {@link Presenter} passed in
* {@link #DetailsOverviewRowPresenter(Presenter)}. Typically this will be an instance of
* {@link AbstractDetailsDescriptionPresenter}. The application can access the
* detailed description ViewHolder from {@link ViewHolder#mDetailsDescriptionViewHolder}.
* </p>
*
* <p>
* To participate in activity transition, call {@link #setSharedElementEnterTransition(Activity,
* String)} during Activity's onCreate().
* </p>
*
* <p>
* Because transition support and layout are fully controlled by DetailsOverviewRowPresenter,
* developer can not override DetailsOverviewRowPresenter.ViewHolder for adding/replacing views
* of DetailsOverviewRowPresenter. If further customization is required beyond replacing
* the detailed description, the application should create a new row presenter class.
* </p>
* @deprecated Use {@link FullWidthDetailsOverviewRowPresenter}
*/
@Deprecated
public class DetailsOverviewRowPresenter extends RowPresenter {
private static final String TAG = "DetailsOverviewRowPresenter";
private static final boolean DEBUG = false;
private static final int MORE_ACTIONS_FADE_MS = 100;
private static final long DEFAULT_TIMEOUT = 5000;
class ActionsItemBridgeAdapter extends ItemBridgeAdapter {
DetailsOverviewRowPresenter.ViewHolder mViewHolder;
ActionsItemBridgeAdapter(DetailsOverviewRowPresenter.ViewHolder viewHolder) {
mViewHolder = viewHolder;
}
@Override
public void onBind(final ItemBridgeAdapter.ViewHolder ibvh) {
if (mViewHolder.getOnItemViewClickedListener() != null ||
mActionClickedListener != null) {
ibvh.getPresenter().setOnClickListener(
ibvh.getViewHolder(), new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mViewHolder.getOnItemViewClickedListener() != null) {
mViewHolder.getOnItemViewClickedListener().onItemClicked(
ibvh.getViewHolder(), ibvh.getItem(),
mViewHolder, mViewHolder.getRow());
}
if (mActionClickedListener != null) {
mActionClickedListener.onActionClicked((Action) ibvh.getItem());
}
}
});
}
}
@Override
public void onUnbind(final ItemBridgeAdapter.ViewHolder ibvh) {
if (mViewHolder.getOnItemViewClickedListener() != null ||
mActionClickedListener != null) {
ibvh.getPresenter().setOnClickListener(ibvh.getViewHolder(), null);
}
}
@Override
public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
// Remove first to ensure we don't add ourselves more than once.
viewHolder.itemView.removeOnLayoutChangeListener(mViewHolder.mLayoutChangeListener);
viewHolder.itemView.addOnLayoutChangeListener(mViewHolder.mLayoutChangeListener);
}
@Override
public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
viewHolder.itemView.removeOnLayoutChangeListener(mViewHolder.mLayoutChangeListener);
mViewHolder.checkFirstAndLastPosition(false);
}
}
/**
* A ViewHolder for the DetailsOverviewRow.
*/
public final class ViewHolder extends RowPresenter.ViewHolder {
final FrameLayout mOverviewFrame;
final ViewGroup mOverviewView;
final ImageView mImageView;
final ViewGroup mRightPanel;
final FrameLayout mDetailsDescriptionFrame;
final HorizontalGridView mActionsRow;
public final Presenter.ViewHolder mDetailsDescriptionViewHolder;
int mNumItems;
boolean mShowMoreRight;
boolean mShowMoreLeft;
ItemBridgeAdapter mActionBridgeAdapter;
final Handler mHandler = new Handler();
final Runnable mUpdateDrawableCallback = new Runnable() {
@Override
public void run() {
bindImageDrawable(ViewHolder.this);
}
};
final DetailsOverviewRow.Listener mListener = new DetailsOverviewRow.Listener() {
@Override
public void onImageDrawableChanged(DetailsOverviewRow row) {
mHandler.removeCallbacks(mUpdateDrawableCallback);
mHandler.post(mUpdateDrawableCallback);
}
@Override
public void onItemChanged(DetailsOverviewRow row) {
if (mDetailsDescriptionViewHolder != null) {
mDetailsPresenter.onUnbindViewHolder(mDetailsDescriptionViewHolder);
}
mDetailsPresenter.onBindViewHolder(mDetailsDescriptionViewHolder, row.getItem());
}
@Override
public void onActionsAdapterChanged(DetailsOverviewRow row) {
bindActions(row.getActionsAdapter());
}
};
void bindActions(ObjectAdapter adapter) {
mActionBridgeAdapter.setAdapter(adapter);
mActionsRow.setAdapter(mActionBridgeAdapter);
mNumItems = mActionBridgeAdapter.getItemCount();
mShowMoreRight = false;
mShowMoreLeft = true;
showMoreLeft(false);
}
final View.OnLayoutChangeListener mLayoutChangeListener =
new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right,
int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
if (DEBUG) Log.v(TAG, "onLayoutChange " + v);
checkFirstAndLastPosition(false);
}
};
final OnChildSelectedListener mChildSelectedListener = new OnChildSelectedListener() {
@Override
public void onChildSelected(ViewGroup parent, View view, int position, long id) {
dispatchItemSelection(view);
}
};
void dispatchItemSelection(View view) {
if (!isSelected()) {
return;
}
ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) (view != null ?
mActionsRow.getChildViewHolder(view) :
mActionsRow.findViewHolderForPosition(mActionsRow.getSelectedPosition()));
if (ibvh == null) {
if (getOnItemViewSelectedListener() != null) {
getOnItemViewSelectedListener().onItemSelected(null, null,
ViewHolder.this, getRow());
}
} else {
if (getOnItemViewSelectedListener() != null) {
getOnItemViewSelectedListener().onItemSelected(ibvh.getViewHolder(), ibvh.getItem(),
ViewHolder.this, getRow());
}
}
};
final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
checkFirstAndLastPosition(true);
}
};
private int getViewCenter(View view) {
return (view.getRight() - view.getLeft()) / 2;
}
private void checkFirstAndLastPosition(boolean fromScroll) {
RecyclerView.ViewHolder viewHolder;
viewHolder = mActionsRow.findViewHolderForPosition(mNumItems - 1);
boolean showRight = (viewHolder == null ||
viewHolder.itemView.getRight() > mActionsRow.getWidth());
viewHolder = mActionsRow.findViewHolderForPosition(0);
boolean showLeft = (viewHolder == null || viewHolder.itemView.getLeft() < 0);
if (DEBUG) Log.v(TAG, "checkFirstAndLast fromScroll " + fromScroll +
" showRight " + showRight + " showLeft " + showLeft);
showMoreRight(showRight);
showMoreLeft(showLeft);
}
private void showMoreLeft(boolean show) {
if (show != mShowMoreLeft) {
mActionsRow.setFadingLeftEdge(show);
mShowMoreLeft = show;
}
}
private void showMoreRight(boolean show) {
if (show != mShowMoreRight) {
mActionsRow.setFadingRightEdge(show);
mShowMoreRight = show;
}
}
/**
* Constructor for the ViewHolder.
*
* @param rootView The root View that this view holder will be attached
* to.
*/
public ViewHolder(View rootView, Presenter detailsPresenter) {
super(rootView);
mOverviewFrame = (FrameLayout) rootView.findViewById(R.id.details_frame);
mOverviewView = (ViewGroup) rootView.findViewById(R.id.details_overview);
mImageView = (ImageView) rootView.findViewById(R.id.details_overview_image);
mRightPanel = (ViewGroup) rootView.findViewById(R.id.details_overview_right_panel);
mDetailsDescriptionFrame =
(FrameLayout) mRightPanel.findViewById(R.id.details_overview_description);
mActionsRow =
(HorizontalGridView) mRightPanel.findViewById(R.id.details_overview_actions);
mActionsRow.setHasOverlappingRendering(false);
mActionsRow.setOnScrollListener(mScrollListener);
mActionsRow.setAdapter(mActionBridgeAdapter);
mActionsRow.setOnChildSelectedListener(mChildSelectedListener);
final int fadeLength = rootView.getResources().getDimensionPixelSize(
R.dimen.lb_details_overview_actions_fade_size);
mActionsRow.setFadingRightEdgeLength(fadeLength);
mActionsRow.setFadingLeftEdgeLength(fadeLength);
mDetailsDescriptionViewHolder =
detailsPresenter.onCreateViewHolder(mDetailsDescriptionFrame);
mDetailsDescriptionFrame.addView(mDetailsDescriptionViewHolder.view);
}
}
private final Presenter mDetailsPresenter;
private OnActionClickedListener mActionClickedListener;
private int mBackgroundColor = Color.TRANSPARENT;
private boolean mBackgroundColorSet;
private boolean mIsStyleLarge = true;
private DetailsOverviewSharedElementHelper mSharedElementHelper;
/**
* Constructor for a DetailsOverviewRowPresenter.
*
* @param detailsPresenter The {@link Presenter} used to render the detailed
* description of the row.
*/
public DetailsOverviewRowPresenter(Presenter detailsPresenter) {
setHeaderPresenter(null);
setSelectEffectEnabled(false);
mDetailsPresenter = detailsPresenter;
}
/**
* Sets the listener for Action click events.
*/
public void setOnActionClickedListener(OnActionClickedListener listener) {
mActionClickedListener = listener;
}
/**
* Returns the listener for Action click events.
*/
public OnActionClickedListener getOnActionClickedListener() {
return mActionClickedListener;
}
/**
* Sets the background color. If not set, a default from the theme will be used.
*/
public void setBackgroundColor(@ColorInt int color) {
mBackgroundColor = color;
mBackgroundColorSet = true;
}
/**
* Returns the background color. If no background color was set, transparent
* is returned.
*/
@ColorInt
public int getBackgroundColor() {
return mBackgroundColor;
}
/**
* Sets the layout style to be large or small. This affects the height of
* the overview, including the text description. The default is large.
*/
public void setStyleLarge(boolean large) {
mIsStyleLarge = large;
}
/**
* Returns true if the layout style is large.
*/
public boolean isStyleLarge() {
return mIsStyleLarge;
}
/**
* Sets the enter transition of target activity to be
* transiting into overview row created by this presenter. The transition will
* be cancelled if the overview image is not loaded in the timeout period.
* <p>
* It assumes shared element passed from calling activity is an ImageView;
* the shared element transits to overview image on the starting edge of the detail
* overview row, while bounds of overview row grows and reveals text
* and action buttons.
* <p>
* The method must be invoked in target Activity's onCreate().
*/
public final void setSharedElementEnterTransition(Activity activity,
String sharedElementName, long timeoutMs) {
if (mSharedElementHelper == null) {
mSharedElementHelper = new DetailsOverviewSharedElementHelper();
}
mSharedElementHelper.setSharedElementEnterTransition(activity, sharedElementName,
timeoutMs);
}
/**
* Sets the enter transition of target activity to be
* transiting into overview row created by this presenter. The transition will
* be cancelled if overview image is not loaded in a default timeout period.
* <p>
* It assumes shared element passed from calling activity is an ImageView;
* the shared element transits to overview image on the starting edge of the detail
* overview row, while bounds of overview row grows and reveals text
* and action buttons.
* <p>
* The method must be invoked in target Activity's onCreate().
*/
public final void setSharedElementEnterTransition(Activity activity,
String sharedElementName) {
setSharedElementEnterTransition(activity, sharedElementName, DEFAULT_TIMEOUT);
}
private int getDefaultBackgroundColor(Context context) {
TypedValue outValue = new TypedValue();
if (context.getTheme().resolveAttribute(R.attr.defaultBrandColor, outValue, true)) {
return context.getResources().getColor(outValue.resourceId);
}
return context.getResources().getColor(R.color.lb_default_brand_color);
}
@Override
protected void onRowViewSelected(RowPresenter.ViewHolder vh, boolean selected) {
super.onRowViewSelected(vh, selected);
if (selected) {
((ViewHolder) vh).dispatchItemSelection(null);
}
}
@Override
protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.lb_details_overview, parent, false);
ViewHolder vh = new ViewHolder(v, mDetailsPresenter);
initDetailsOverview(vh);
return vh;
}
private int getCardHeight(Context context) {
int resId = mIsStyleLarge ? R.dimen.lb_details_overview_height_large :
R.dimen.lb_details_overview_height_small;
return context.getResources().getDimensionPixelSize(resId);
}
private void initDetailsOverview(final ViewHolder vh) {
vh.mActionBridgeAdapter = new ActionsItemBridgeAdapter(vh);
final View overview = vh.mOverviewFrame;
ViewGroup.LayoutParams lp = overview.getLayoutParams();
lp.height = getCardHeight(overview.getContext());
overview.setLayoutParams(lp);
if (!getSelectEffectEnabled()) {
vh.mOverviewFrame.setForeground(null);
}
vh.mActionsRow.setOnUnhandledKeyListener(new BaseGridView.OnUnhandledKeyListener() {
@Override
public boolean onUnhandledKey(KeyEvent event) {
if (vh.getOnKeyListener() != null) {
if (vh.getOnKeyListener().onKey(vh.view, event.getKeyCode(), event)) {
return true;
}
}
return false;
}
});
}
private static int getNonNegativeWidth(Drawable drawable) {
final int width = (drawable == null) ? 0 : drawable.getIntrinsicWidth();
return (width > 0 ? width : 0);
}
private static int getNonNegativeHeight(Drawable drawable) {
final int height = (drawable == null) ? 0 : drawable.getIntrinsicHeight();
return (height > 0 ? height : 0);
}
private void bindImageDrawable(ViewHolder vh) {
DetailsOverviewRow row = (DetailsOverviewRow) vh.getRow();
ViewGroup.MarginLayoutParams layoutParams =
(ViewGroup.MarginLayoutParams) vh.mImageView.getLayoutParams();
final int cardHeight = getCardHeight(vh.mImageView.getContext());
final int verticalMargin = vh.mImageView.getResources().getDimensionPixelSize(
R.dimen.lb_details_overview_image_margin_vertical);
final int horizontalMargin = vh.mImageView.getResources().getDimensionPixelSize(
R.dimen.lb_details_overview_image_margin_horizontal);
final int drawableWidth = getNonNegativeWidth(row.getImageDrawable());
final int drawableHeight = getNonNegativeHeight(row.getImageDrawable());
boolean scaleImage = row.isImageScaleUpAllowed();
boolean useMargin = false;
if (row.getImageDrawable() != null) {
boolean landscape = false;
// If large style and landscape image we always use margin.
if (drawableWidth > drawableHeight) {
landscape = true;
if (mIsStyleLarge) {
useMargin = true;
}
}
// If long dimension bigger than the card height we scale down.
if ((landscape && drawableWidth > cardHeight) ||
(!landscape && drawableHeight > cardHeight)) {
scaleImage = true;
}
// If we're not scaling to fit the card height then we always use margin.
if (!scaleImage) {
useMargin = true;
}
// If using margin than may need to scale down.
if (useMargin && !scaleImage) {
if (landscape && drawableWidth > cardHeight - horizontalMargin) {
scaleImage = true;
} else if (!landscape && drawableHeight > cardHeight - 2 * verticalMargin) {
scaleImage = true;
}
}
}
final int bgColor = mBackgroundColorSet ? mBackgroundColor :
getDefaultBackgroundColor(vh.mOverviewView.getContext());
if (useMargin) {
layoutParams.setMarginStart(horizontalMargin);
layoutParams.topMargin = layoutParams.bottomMargin = verticalMargin;
vh.mOverviewFrame.setBackgroundColor(bgColor);
vh.mRightPanel.setBackground(null);
vh.mImageView.setBackground(null);
} else {
layoutParams.leftMargin = layoutParams.topMargin = layoutParams.bottomMargin = 0;
vh.mRightPanel.setBackgroundColor(bgColor);
vh.mImageView.setBackgroundColor(bgColor);
vh.mOverviewFrame.setBackground(null);
}
RoundedRectHelper.getInstance().setClipToRoundedOutline(vh.mOverviewFrame, true);
if (scaleImage) {
vh.mImageView.setScaleType(ImageView.ScaleType.FIT_START);
vh.mImageView.setAdjustViewBounds(true);
vh.mImageView.setMaxWidth(cardHeight);
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
} else {
vh.mImageView.setScaleType(ImageView.ScaleType.CENTER);
vh.mImageView.setAdjustViewBounds(false);
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
// Limit width to the card height
layoutParams.width = Math.min(cardHeight, drawableWidth);
}
vh.mImageView.setLayoutParams(layoutParams);
vh.mImageView.setImageDrawable(row.getImageDrawable());
if (row.getImageDrawable() != null && mSharedElementHelper != null) {
mSharedElementHelper.onBindToDrawable(vh);
}
}
@Override
protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
super.onBindRowViewHolder(holder, item);
DetailsOverviewRow row = (DetailsOverviewRow) item;
ViewHolder vh = (ViewHolder) holder;
bindImageDrawable(vh);
mDetailsPresenter.onBindViewHolder(vh.mDetailsDescriptionViewHolder, row.getItem());
vh.bindActions(row.getActionsAdapter());
row.addListener(vh.mListener);
}
@Override
protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) {
ViewHolder vh = (ViewHolder) holder;
DetailsOverviewRow dor = (DetailsOverviewRow) vh.getRow();
dor.removeListener(vh.mListener);
if (vh.mDetailsDescriptionViewHolder != null) {
mDetailsPresenter.onUnbindViewHolder(vh.mDetailsDescriptionViewHolder);
}
super.onUnbindRowViewHolder(holder);
}
@Override
public final boolean isUsingDefaultSelectEffect() {
return false;
}
@Override
protected void onSelectLevelChanged(RowPresenter.ViewHolder holder) {
super.onSelectLevelChanged(holder);
if (getSelectEffectEnabled()) {
ViewHolder vh = (ViewHolder) holder;
int dimmedColor = vh.mColorDimmer.getPaint().getColor();
((ColorDrawable) vh.mOverviewFrame.getForeground().mutate()).setColor(dimmedColor);
}
}
@Override
protected void onRowViewAttachedToWindow(RowPresenter.ViewHolder vh) {
super.onRowViewAttachedToWindow(vh);
if (mDetailsPresenter != null) {
mDetailsPresenter.onViewAttachedToWindow(
((ViewHolder) vh).mDetailsDescriptionViewHolder);
}
}
@Override
protected void onRowViewDetachedFromWindow(RowPresenter.ViewHolder vh) {
super.onRowViewDetachedFromWindow(vh);
if (mDetailsPresenter != null) {
mDetailsPresenter.onViewDetachedFromWindow(
((ViewHolder) vh).mDetailsDescriptionViewHolder);
}
}
}