/* * 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. * *
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}. *
* ** To participate in activity transition, call {@link #setSharedElementEnterTransition(Activity, * String)} during Activity's onCreate(). *
* ** 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. *
* @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. ** 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. *
* 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. *
* 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. *
* 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); } } }