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