DetailsOverviewRowPresenter.java revision 70acb0c19be3831a2080e4f902324de16bfbf62e
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.widget;
15
16import android.app.Activity;
17import android.content.Context;
18import android.graphics.Bitmap;
19import android.graphics.Color;
20import android.graphics.drawable.Drawable;
21import android.graphics.drawable.BitmapDrawable;
22import android.graphics.drawable.ColorDrawable;
23import android.os.Handler;
24import android.support.annotation.ColorInt;
25import android.support.v17.leanback.R;
26import android.support.v7.widget.RecyclerView;
27import android.util.Log;
28import android.util.TypedValue;
29import android.view.KeyEvent;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.ViewGroup;
33import android.widget.FrameLayout;
34import android.widget.ImageView;
35
36import java.util.Collection;
37
38/**
39 * A DetailsOverviewRowPresenter renders a {@link DetailsOverviewRow} to display an
40 * overview of an item. Typically this row will be the first row in a fragment
41 * such as the {@link android.support.v17.leanback.app.DetailsFragment
42 * DetailsFragment}.  View created by DetailsOverviewRowPresenter is made in three parts:
43 * ImageView on the left, action list view on the bottom and a customizable detailed
44 * description view on the right.
45 *
46 * <p>The detailed description is rendered using a {@link Presenter} passed in
47 * {@link #DetailsOverviewRowPresenter(Presenter)}.  User can access detailed description
48 * ViewHolder from {@link ViewHolder#mDetailsDescriptionViewHolder}.
49 * </p>
50 *
51 * <p>
52 * To participate in activity transition, call {@link #setSharedElementEnterTransition(Activity,
53 * String)} during Activity's onCreate().
54 * </p>
55 *
56 * <p>
57 * Because transition support and layout are fully controlled by DetailsOverviewRowPresenter,
58 * developer can not override DetailsOverviewRowPresenter.ViewHolder for adding/replacing views
59 * of DetailsOverviewRowPresenter.  If developer wants more customization beyond replacing
60 * detailed description , he/she should write a new presenter class for row object.
61 * </p>
62 */
63public class DetailsOverviewRowPresenter extends RowPresenter {
64
65    private static final String TAG = "DetailsOverviewRowPresenter";
66    private static final boolean DEBUG = false;
67
68    private static final int MORE_ACTIONS_FADE_MS = 100;
69    private static final long DEFAULT_TIMEOUT = 5000;
70
71    class ActionsItemBridgeAdapter extends ItemBridgeAdapter {
72        DetailsOverviewRowPresenter.ViewHolder mViewHolder;
73
74        ActionsItemBridgeAdapter(DetailsOverviewRowPresenter.ViewHolder viewHolder) {
75            mViewHolder = viewHolder;
76        }
77
78        @Override
79        public void onBind(final ItemBridgeAdapter.ViewHolder ibvh) {
80            if (getOnItemViewClickedListener() != null || mActionClickedListener != null) {
81                ibvh.getPresenter().setOnClickListener(
82                        ibvh.getViewHolder(), new View.OnClickListener() {
83                            @Override
84                            public void onClick(View v) {
85                                if (getOnItemViewClickedListener() != null) {
86                                    getOnItemViewClickedListener().onItemClicked(
87                                            ibvh.getViewHolder(), ibvh.getItem(),
88                                            mViewHolder, mViewHolder.getRow());
89                                }
90                                if (mActionClickedListener != null) {
91                                    mActionClickedListener.onActionClicked((Action) ibvh.getItem());
92                                }
93                            }
94                        });
95            }
96        }
97        @Override
98        public void onUnbind(final ItemBridgeAdapter.ViewHolder ibvh) {
99            if (getOnItemViewClickedListener() != null || mActionClickedListener != null) {
100                ibvh.getPresenter().setOnClickListener(ibvh.getViewHolder(), null);
101            }
102        }
103        @Override
104        public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
105            // Remove first to ensure we don't add ourselves more than once.
106            viewHolder.itemView.removeOnLayoutChangeListener(mViewHolder.mLayoutChangeListener);
107            viewHolder.itemView.addOnLayoutChangeListener(mViewHolder.mLayoutChangeListener);
108        }
109        @Override
110        public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
111            viewHolder.itemView.removeOnLayoutChangeListener(mViewHolder.mLayoutChangeListener);
112            mViewHolder.checkFirstAndLastPosition(false);
113        }
114    }
115
116    /**
117     * A ViewHolder for the DetailsOverviewRow.
118     */
119    public final class ViewHolder extends RowPresenter.ViewHolder {
120        final FrameLayout mOverviewFrame;
121        final ViewGroup mOverviewView;
122        final ImageView mImageView;
123        final ViewGroup mRightPanel;
124        final FrameLayout mDetailsDescriptionFrame;
125        final HorizontalGridView mActionsRow;
126        public final Presenter.ViewHolder mDetailsDescriptionViewHolder;
127        int mNumItems;
128        boolean mShowMoreRight;
129        boolean mShowMoreLeft;
130        ItemBridgeAdapter mActionBridgeAdapter;
131        final Handler mHandler = new Handler();
132
133        final Runnable mUpdateDrawableCallback = new Runnable() {
134            @Override
135            public void run() {
136                bindImageDrawable(ViewHolder.this);
137            }
138        };
139
140        final DetailsOverviewRow.Listener mListener = new DetailsOverviewRow.Listener() {
141            @Override
142            public void onImageDrawableChanged(DetailsOverviewRow row) {
143                mHandler.removeCallbacks(mUpdateDrawableCallback);
144                mHandler.post(mUpdateDrawableCallback);
145            }
146
147            @Override
148            public void onItemChanged(DetailsOverviewRow row) {
149                if (mDetailsDescriptionViewHolder != null) {
150                    mDetailsPresenter.onUnbindViewHolder(mDetailsDescriptionViewHolder);
151                }
152                mDetailsPresenter.onBindViewHolder(mDetailsDescriptionViewHolder, row.getItem());
153            }
154
155            @Override
156            public void onActionsAdapterChanged(DetailsOverviewRow row) {
157                bindActions(row.getActionsAdapter());
158            }
159        };
160
161        void bindActions(ObjectAdapter adapter) {
162            mActionBridgeAdapter.setAdapter(adapter);
163            mActionsRow.setAdapter(mActionBridgeAdapter);
164            mNumItems = mActionBridgeAdapter.getItemCount();
165
166            mShowMoreRight = false;
167            mShowMoreLeft = true;
168            showMoreLeft(false);
169        }
170
171        final View.OnLayoutChangeListener mLayoutChangeListener =
172                new View.OnLayoutChangeListener() {
173
174            @Override
175            public void onLayoutChange(View v, int left, int top, int right,
176                    int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
177                if (DEBUG) Log.v(TAG, "onLayoutChange " + v);
178                checkFirstAndLastPosition(false);
179            }
180        };
181
182        final OnChildSelectedListener mChildSelectedListener = new OnChildSelectedListener() {
183            @Override
184            public void onChildSelected(ViewGroup parent, View view, int position, long id) {
185                dispatchItemSelection(view);
186            }
187        };
188
189        void dispatchItemSelection(View view) {
190            if (!isSelected()) {
191                return;
192            }
193            ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) (view != null ?
194                    mActionsRow.getChildViewHolder(view) :
195                    mActionsRow.findViewHolderForPosition(mActionsRow.getSelectedPosition()));
196            if (ibvh == null) {
197                if (getOnItemViewSelectedListener() != null) {
198                    getOnItemViewSelectedListener().onItemSelected(null, null,
199                            ViewHolder.this, getRow());
200                }
201            } else {
202                if (getOnItemViewSelectedListener() != null) {
203                    getOnItemViewSelectedListener().onItemSelected(ibvh.getViewHolder(), ibvh.getItem(),
204                            ViewHolder.this, getRow());
205                }
206            }
207        };
208
209        final RecyclerView.OnScrollListener mScrollListener =
210                new RecyclerView.OnScrollListener() {
211
212            @Override
213            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
214            }
215            @Override
216            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
217                checkFirstAndLastPosition(true);
218            }
219        };
220
221        private int getViewCenter(View view) {
222            return (view.getRight() - view.getLeft()) / 2;
223        }
224
225        private void checkFirstAndLastPosition(boolean fromScroll) {
226            RecyclerView.ViewHolder viewHolder;
227
228            viewHolder = mActionsRow.findViewHolderForPosition(mNumItems - 1);
229            boolean showRight = (viewHolder == null ||
230                    viewHolder.itemView.getRight() > mActionsRow.getWidth());
231
232            viewHolder = mActionsRow.findViewHolderForPosition(0);
233            boolean showLeft = (viewHolder == null || viewHolder.itemView.getLeft() < 0);
234
235            if (DEBUG) Log.v(TAG, "checkFirstAndLast fromScroll " + fromScroll +
236                    " showRight " + showRight + " showLeft " + showLeft);
237
238            showMoreRight(showRight);
239            showMoreLeft(showLeft);
240        }
241
242        private void showMoreLeft(boolean show) {
243            if (show != mShowMoreLeft) {
244                mActionsRow.setFadingLeftEdge(show);
245                mShowMoreLeft = show;
246            }
247        }
248
249        private void showMoreRight(boolean show) {
250            if (show != mShowMoreRight) {
251                mActionsRow.setFadingRightEdge(show);
252                mShowMoreRight = show;
253            }
254        }
255
256        /**
257         * Constructor for the ViewHolder.
258         *
259         * @param rootView The root View that this view holder will be attached
260         *        to.
261         */
262        public ViewHolder(View rootView, Presenter detailsPresenter) {
263            super(rootView);
264            mOverviewFrame = (FrameLayout) rootView.findViewById(R.id.details_frame);
265            mOverviewView = (ViewGroup) rootView.findViewById(R.id.details_overview);
266            mImageView = (ImageView) rootView.findViewById(R.id.details_overview_image);
267            mRightPanel = (ViewGroup) rootView.findViewById(R.id.details_overview_right_panel);
268            mDetailsDescriptionFrame =
269                    (FrameLayout) mRightPanel.findViewById(R.id.details_overview_description);
270            mActionsRow =
271                    (HorizontalGridView) mRightPanel.findViewById(R.id.details_overview_actions);
272            mActionsRow.setHasOverlappingRendering(false);
273            mActionsRow.setOnScrollListener(mScrollListener);
274            mActionsRow.setAdapter(mActionBridgeAdapter);
275            mActionsRow.setOnChildSelectedListener(mChildSelectedListener);
276
277            final int fadeLength = rootView.getResources().getDimensionPixelSize(
278                    R.dimen.lb_details_overview_actions_fade_size);
279            mActionsRow.setFadingRightEdgeLength(fadeLength);
280            mActionsRow.setFadingLeftEdgeLength(fadeLength);
281            mDetailsDescriptionViewHolder =
282                    detailsPresenter.onCreateViewHolder(mDetailsDescriptionFrame);
283            mDetailsDescriptionFrame.addView(mDetailsDescriptionViewHolder.view);
284        }
285    }
286
287    private final Presenter mDetailsPresenter;
288    private OnActionClickedListener mActionClickedListener;
289
290    private int mBackgroundColor = Color.TRANSPARENT;
291    private boolean mBackgroundColorSet;
292    private boolean mIsStyleLarge = true;
293
294    private DetailsOverviewSharedElementHelper mSharedElementHelper;
295
296    /**
297     * Constructor for a DetailsOverviewRowPresenter.
298     *
299     * @param detailsPresenter The {@link Presenter} used to render the detailed
300     *        description of the row.
301     */
302    public DetailsOverviewRowPresenter(Presenter detailsPresenter) {
303        setHeaderPresenter(null);
304        setSelectEffectEnabled(false);
305        mDetailsPresenter = detailsPresenter;
306    }
307
308    /**
309     * Sets the listener for Action click events.
310     */
311    public void setOnActionClickedListener(OnActionClickedListener listener) {
312        mActionClickedListener = listener;
313    }
314
315    /**
316     * Gets the listener for Action click events.
317     */
318    public OnActionClickedListener getOnActionClickedListener() {
319        return mActionClickedListener;
320    }
321
322    /**
323     * Sets the background color.  If not set, a default from the theme will be used.
324     */
325    public void setBackgroundColor(@ColorInt int color) {
326        mBackgroundColor = color;
327        mBackgroundColorSet = true;
328    }
329
330    /**
331     * Returns the background color.  If no background color was set, transparent
332     * is returned.
333     */
334    @ColorInt
335    public int getBackgroundColor() {
336        return mBackgroundColor;
337    }
338
339    /**
340     * Sets the layout style to be large or small. This affects the height of
341     * the overview, including the text description. The default is large.
342     */
343    public void setStyleLarge(boolean large) {
344        mIsStyleLarge = large;
345    }
346
347    /**
348     * Returns true if the layout style is large.
349     */
350    public boolean isStyleLarge() {
351        return mIsStyleLarge;
352    }
353
354    /**
355     * Set enter transition of target activity (typically a DetailActivity) to be
356     * transiting into overview row created by this presenter.  The transition will
357     * be cancelled if overview image is not loaded in the timeout period.
358     * <p>
359     * It assumes shared element passed from calling activity is an ImageView;
360     * the shared element transits to overview image on the left of detail
361     * overview row, while bounds of overview row grows and reveals text
362     * and buttons on the right.
363     * <p>
364     * The method must be invoked in target Activity's onCreate().
365     */
366    public final void setSharedElementEnterTransition(Activity activity,
367            String sharedElementName, long timeoutMs) {
368        if (mSharedElementHelper == null) {
369            mSharedElementHelper = new DetailsOverviewSharedElementHelper();
370        }
371        mSharedElementHelper.setSharedElementEnterTransition(activity, sharedElementName,
372                timeoutMs);
373    }
374
375    /**
376     * Set enter transition of target activity (typically a DetailActivity) to be
377     * transiting into overview row created by this presenter.  The transition will
378     * be cancelled if overview image is not loaded in a default timeout period.
379     * <p>
380     * It assumes shared element passed from calling activity is an ImageView;
381     * the shared element transits to overview image on the left of detail
382     * overview row, while bounds of overview row grows and reveals text
383     * and buttons on the right.
384     * <p>
385     * The method must be invoked in target Activity's onCreate().
386     */
387    public final void setSharedElementEnterTransition(Activity activity,
388            String sharedElementName) {
389        setSharedElementEnterTransition(activity, sharedElementName, DEFAULT_TIMEOUT);
390    }
391
392    private int getDefaultBackgroundColor(Context context) {
393        TypedValue outValue = new TypedValue();
394        if (context.getTheme().resolveAttribute(R.attr.defaultBrandColor, outValue, true)) {
395            return context.getResources().getColor(outValue.resourceId);
396        }
397        return context.getResources().getColor(R.color.lb_default_brand_color);
398    }
399
400    @Override
401    protected void onRowViewSelected(RowPresenter.ViewHolder vh, boolean selected) {
402        super.onRowViewSelected(vh, selected);
403        if (selected) {
404            ((ViewHolder) vh).dispatchItemSelection(null);
405        }
406    }
407
408    @Override
409    protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
410        View v = LayoutInflater.from(parent.getContext())
411            .inflate(R.layout.lb_details_overview, parent, false);
412        ViewHolder vh = new ViewHolder(v, mDetailsPresenter);
413
414        initDetailsOverview(vh);
415
416        return vh;
417    }
418
419    private int getCardHeight(Context context) {
420        int resId = mIsStyleLarge ? R.dimen.lb_details_overview_height_large :
421            R.dimen.lb_details_overview_height_small;
422        return context.getResources().getDimensionPixelSize(resId);
423    }
424
425    private void initDetailsOverview(final ViewHolder vh) {
426        vh.mActionBridgeAdapter = new ActionsItemBridgeAdapter(vh);
427        final View overview = vh.mOverviewFrame;
428        ViewGroup.LayoutParams lp = overview.getLayoutParams();
429        lp.height = getCardHeight(overview.getContext());
430        overview.setLayoutParams(lp);
431
432        if (!getSelectEffectEnabled()) {
433            vh.mOverviewFrame.setForeground(null);
434        }
435        vh.mActionsRow.setOnUnhandledKeyListener(new BaseGridView.OnUnhandledKeyListener() {
436            @Override
437            public boolean onUnhandledKey(KeyEvent event) {
438                if (vh.getOnKeyListener() != null) {
439                    if (vh.getOnKeyListener().onKey(vh.view, event.getKeyCode(), event)) {
440                        return true;
441                    }
442                }
443                return false;
444            }
445        });
446    }
447
448    private static int getNonNegativeWidth(Drawable drawable) {
449        final int width = (drawable == null) ? 0 : drawable.getIntrinsicWidth();
450        return (width > 0 ? width : 0);
451    }
452
453    private static int getNonNegativeHeight(Drawable drawable) {
454        final int height = (drawable == null) ? 0 : drawable.getIntrinsicHeight();
455        return (height > 0 ? height : 0);
456    }
457
458    private void bindImageDrawable(ViewHolder vh) {
459        DetailsOverviewRow row = (DetailsOverviewRow) vh.getRow();
460
461        ViewGroup.MarginLayoutParams layoutParams =
462                (ViewGroup.MarginLayoutParams) vh.mImageView.getLayoutParams();
463        final int cardHeight = getCardHeight(vh.mImageView.getContext());
464        final int verticalMargin = vh.mImageView.getResources().getDimensionPixelSize(
465                R.dimen.lb_details_overview_image_margin_vertical);
466        final int horizontalMargin = vh.mImageView.getResources().getDimensionPixelSize(
467                R.dimen.lb_details_overview_image_margin_horizontal);
468        final int drawableWidth = getNonNegativeWidth(row.getImageDrawable());
469        final int drawableHeight = getNonNegativeHeight(row.getImageDrawable());
470
471        boolean scaleImage = row.isImageScaleUpAllowed();
472        boolean useMargin = false;
473
474        if (row.getImageDrawable() != null) {
475            boolean landscape = false;
476
477            // If large style and landscape image we always use margin.
478            if (drawableWidth > drawableHeight) {
479                landscape = true;
480                if (mIsStyleLarge) {
481                    useMargin = true;
482                }
483            }
484            // If long dimension bigger than the card height we scale down.
485            if ((landscape && drawableWidth > cardHeight) ||
486                    (!landscape && drawableHeight > cardHeight)) {
487                scaleImage = true;
488            }
489            // If we're not scaling to fit the card height then we always use margin.
490            if (!scaleImage) {
491                useMargin = true;
492            }
493            // If using margin than may need to scale down.
494            if (useMargin && !scaleImage) {
495                if (landscape && drawableWidth > cardHeight - horizontalMargin) {
496                    scaleImage = true;
497                } else if (!landscape && drawableHeight > cardHeight - 2 * verticalMargin) {
498                    scaleImage = true;
499                }
500            }
501        }
502
503        final int bgColor = mBackgroundColorSet ? mBackgroundColor :
504            getDefaultBackgroundColor(vh.mOverviewView.getContext());
505
506        if (useMargin) {
507            layoutParams.setMarginStart(horizontalMargin);
508            layoutParams.topMargin = layoutParams.bottomMargin = verticalMargin;
509            RoundedRectHelper.getInstance().setRoundedRectBackground(vh.mOverviewFrame, bgColor);
510            vh.mRightPanel.setBackground(null);
511            vh.mImageView.setBackground(null);
512        } else {
513            layoutParams.leftMargin = layoutParams.topMargin = layoutParams.bottomMargin = 0;
514            vh.mRightPanel.setBackgroundColor(bgColor);
515            vh.mImageView.setBackgroundColor(bgColor);
516            RoundedRectHelper.getInstance().setRoundedRectBackground(vh.mOverviewFrame,
517                    Color.TRANSPARENT);
518        }
519        if (scaleImage) {
520            vh.mImageView.setScaleType(ImageView.ScaleType.FIT_START);
521            vh.mImageView.setAdjustViewBounds(true);
522            vh.mImageView.setMaxWidth(cardHeight);
523            layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
524            layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
525        } else {
526            vh.mImageView.setScaleType(ImageView.ScaleType.CENTER);
527            vh.mImageView.setAdjustViewBounds(false);
528            layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
529            // Limit width to the card height
530            layoutParams.width = Math.min(cardHeight, drawableWidth);
531        }
532        vh.mImageView.setLayoutParams(layoutParams);
533        vh.mImageView.setImageDrawable(row.getImageDrawable());
534        if (row.getImageDrawable() != null && mSharedElementHelper != null) {
535            mSharedElementHelper.onBindToDrawable(vh);
536        }
537    }
538
539    @Override
540    protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
541        super.onBindRowViewHolder(holder, item);
542
543        DetailsOverviewRow row = (DetailsOverviewRow) item;
544        ViewHolder vh = (ViewHolder) holder;
545
546        bindImageDrawable(vh);
547        mDetailsPresenter.onBindViewHolder(vh.mDetailsDescriptionViewHolder, row.getItem());
548        vh.bindActions(row.getActionsAdapter());
549        row.addListener(vh.mListener);
550    }
551
552    @Override
553    protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) {
554        ViewHolder vh = (ViewHolder) holder;
555        DetailsOverviewRow dor = (DetailsOverviewRow) vh.getRow();
556        dor.removeListener(vh.mListener);
557        if (vh.mDetailsDescriptionViewHolder != null) {
558            mDetailsPresenter.onUnbindViewHolder(vh.mDetailsDescriptionViewHolder);
559        }
560        super.onUnbindRowViewHolder(holder);
561    }
562
563    @Override
564    public final boolean isUsingDefaultSelectEffect() {
565        return false;
566    }
567
568    @Override
569    protected void onSelectLevelChanged(RowPresenter.ViewHolder holder) {
570        super.onSelectLevelChanged(holder);
571        if (getSelectEffectEnabled()) {
572            ViewHolder vh = (ViewHolder) holder;
573            int dimmedColor = vh.mColorDimmer.getPaint().getColor();
574            ((ColorDrawable) vh.mOverviewFrame.getForeground().mutate()).setColor(dimmedColor);
575        }
576    }
577
578    @Override
579    protected void onRowViewAttachedToWindow(RowPresenter.ViewHolder vh) {
580        super.onRowViewAttachedToWindow(vh);
581        if (mDetailsPresenter != null) {
582            mDetailsPresenter.onViewAttachedToWindow(
583                    ((ViewHolder) vh).mDetailsDescriptionViewHolder);
584        }
585    }
586
587    @Override
588    protected void onRowViewDetachedFromWindow(RowPresenter.ViewHolder vh) {
589        super.onRowViewDetachedFromWindow(vh);
590        if (mDetailsPresenter != null) {
591            mDetailsPresenter.onViewDetachedFromWindow(
592                    ((ViewHolder) vh).mDetailsDescriptionViewHolder);
593        }
594    }
595}
596