DetailsOverviewRowPresenter.java revision 71fddded48048acfa744ac352166770c91a1c2b1
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.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.FrameLayout;
32import android.widget.ImageView;
33
34import java.util.Collection;
35
36/**
37 * A DetailsOverviewRowPresenter renders a {@link DetailsOverviewRow} to display an
38 * overview of an item. Typically this row will be the first row in a fragment
39 * such as the {@link android.support.v17.leanback.app.DetailsFragment
40 * DetailsFragment}.  View created by DetailsOverviewRowPresenter is made in three parts:
41 * ImageView on the left, action list view on the bottom and a customizable detailed
42 * description view on the right.
43 *
44 * <p>The detailed description is rendered using a {@link Presenter} passed in
45 * {@link #DetailsOverviewRowPresenter(Presenter)}.  User can access detailed description
46 * ViewHolder from {@link ViewHolder#mDetailsDescriptionViewHolder}.
47 * </p>
48 *
49 * <p>
50 * To participate in activity transition, call {@link #setSharedElementEnterTransition(Activity,
51 * String)} during Activity's onCreate().
52 * </p>
53 *
54 * <p>
55 * Because transition support and layout are fully controlled by DetailsOverviewRowPresenter,
56 * developer can not override DetailsOverviewRowPresenter.ViewHolder for adding/replacing views
57 * of DetailsOverviewRowPresenter.  If developer wants more customization beyond replacing
58 * detailed description , he/she should write a new presenter class for row object.
59 * </p>
60 */
61public class DetailsOverviewRowPresenter extends RowPresenter {
62
63    private static final String TAG = "DetailsOverviewRowPresenter";
64    private static final boolean DEBUG = false;
65
66    private static final int MORE_ACTIONS_FADE_MS = 100;
67    private static final long DEFAULT_TIMEOUT = 5000;
68
69    class ActionsItemBridgeAdapter extends ItemBridgeAdapter {
70        DetailsOverviewRowPresenter.ViewHolder mViewHolder;
71
72        ActionsItemBridgeAdapter(DetailsOverviewRowPresenter.ViewHolder viewHolder) {
73            mViewHolder = viewHolder;
74        }
75
76        @Override
77        public void onBind(final ItemBridgeAdapter.ViewHolder ibvh) {
78            if (getOnItemViewClickedListener() != null || getOnItemClickedListener() != null
79                    || mActionClickedListener != null) {
80                ibvh.getPresenter().setOnClickListener(
81                        ibvh.getViewHolder(), new View.OnClickListener() {
82                            @Override
83                            public void onClick(View v) {
84                                if (getOnItemViewClickedListener() != null) {
85                                    getOnItemViewClickedListener().onItemClicked(
86                                            ibvh.getViewHolder(), ibvh.getItem(),
87                                            mViewHolder, mViewHolder.getRow());
88                                }
89                                if (mActionClickedListener != null) {
90                                    mActionClickedListener.onActionClicked((Action) ibvh.getItem());
91                                }
92                            }
93                        });
94            }
95        }
96        @Override
97        public void onUnbind(final ItemBridgeAdapter.ViewHolder ibvh) {
98            if (getOnItemViewClickedListener() != null || getOnItemClickedListener() != null
99                    || 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 (getOnItemSelectedListener() != null) {
198                    getOnItemSelectedListener().onItemSelected(null, getRow());
199                }
200                if (getOnItemViewSelectedListener() != null) {
201                    getOnItemViewSelectedListener().onItemSelected(null, null,
202                            ViewHolder.this, getRow());
203                }
204            } else {
205                if (getOnItemSelectedListener() != null) {
206                    getOnItemSelectedListener().onItemSelected(ibvh.getItem(), getRow());
207                }
208                if (getOnItemViewSelectedListener() != null) {
209                    getOnItemViewSelectedListener().onItemSelected(ibvh.getViewHolder(), ibvh.getItem(),
210                            ViewHolder.this, getRow());
211                }
212            }
213        };
214
215        final RecyclerView.OnScrollListener mScrollListener =
216                new RecyclerView.OnScrollListener() {
217
218            @Override
219            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
220            }
221            @Override
222            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
223                checkFirstAndLastPosition(true);
224            }
225        };
226
227        private int getViewCenter(View view) {
228            return (view.getRight() - view.getLeft()) / 2;
229        }
230
231        private void checkFirstAndLastPosition(boolean fromScroll) {
232            RecyclerView.ViewHolder viewHolder;
233
234            viewHolder = mActionsRow.findViewHolderForPosition(mNumItems - 1);
235            boolean showRight = (viewHolder == null ||
236                    viewHolder.itemView.getRight() > mActionsRow.getWidth());
237
238            viewHolder = mActionsRow.findViewHolderForPosition(0);
239            boolean showLeft = (viewHolder == null || viewHolder.itemView.getLeft() < 0);
240
241            if (DEBUG) Log.v(TAG, "checkFirstAndLast fromScroll " + fromScroll +
242                    " showRight " + showRight + " showLeft " + showLeft);
243
244            showMoreRight(showRight);
245            showMoreLeft(showLeft);
246        }
247
248        private void showMoreLeft(boolean show) {
249            if (show != mShowMoreLeft) {
250                mActionsRow.setFadingLeftEdge(show);
251                mShowMoreLeft = show;
252            }
253        }
254
255        private void showMoreRight(boolean show) {
256            if (show != mShowMoreRight) {
257                mActionsRow.setFadingRightEdge(show);
258                mShowMoreRight = show;
259            }
260        }
261
262        /**
263         * Constructor for the ViewHolder.
264         *
265         * @param rootView The root View that this view holder will be attached
266         *        to.
267         */
268        public ViewHolder(View rootView, Presenter detailsPresenter) {
269            super(rootView);
270            mOverviewFrame = (FrameLayout) rootView.findViewById(R.id.details_frame);
271            mOverviewView = (ViewGroup) rootView.findViewById(R.id.details_overview);
272            mImageView = (ImageView) rootView.findViewById(R.id.details_overview_image);
273            mRightPanel = (ViewGroup) rootView.findViewById(R.id.details_overview_right_panel);
274            mDetailsDescriptionFrame =
275                    (FrameLayout) mRightPanel.findViewById(R.id.details_overview_description);
276            mActionsRow =
277                    (HorizontalGridView) mRightPanel.findViewById(R.id.details_overview_actions);
278            mActionsRow.setHasOverlappingRendering(false);
279            mActionsRow.setOnScrollListener(mScrollListener);
280            mActionsRow.setAdapter(mActionBridgeAdapter);
281            mActionsRow.setOnChildSelectedListener(mChildSelectedListener);
282
283            final int fadeLength = rootView.getResources().getDimensionPixelSize(
284                    R.dimen.lb_details_overview_actions_fade_size);
285            mActionsRow.setFadingRightEdgeLength(fadeLength);
286            mActionsRow.setFadingLeftEdgeLength(fadeLength);
287            mDetailsDescriptionViewHolder =
288                    detailsPresenter.onCreateViewHolder(mDetailsDescriptionFrame);
289            mDetailsDescriptionFrame.addView(mDetailsDescriptionViewHolder.view);
290        }
291    }
292
293    private final Presenter mDetailsPresenter;
294    private OnActionClickedListener mActionClickedListener;
295
296    private int mBackgroundColor = Color.TRANSPARENT;
297    private boolean mBackgroundColorSet;
298    private boolean mIsStyleLarge = true;
299
300    private DetailsOverviewSharedElementHelper mSharedElementHelper;
301
302    /**
303     * Constructor for a DetailsOverviewRowPresenter.
304     *
305     * @param detailsPresenter The {@link Presenter} used to render the detailed
306     *        description of the row.
307     */
308    public DetailsOverviewRowPresenter(Presenter detailsPresenter) {
309        setHeaderPresenter(null);
310        setSelectEffectEnabled(false);
311        mDetailsPresenter = detailsPresenter;
312    }
313
314    /**
315     * Sets the listener for Action click events.
316     */
317    public void setOnActionClickedListener(OnActionClickedListener listener) {
318        mActionClickedListener = listener;
319    }
320
321    /**
322     * Gets the listener for Action click events.
323     */
324    public OnActionClickedListener getOnActionClickedListener() {
325        return mActionClickedListener;
326    }
327
328    /**
329     * Sets the background color.  If not set, a default from the theme will be used.
330     */
331    public void setBackgroundColor(int color) {
332        mBackgroundColor = color;
333        mBackgroundColorSet = true;
334    }
335
336    /**
337     * Returns the background color.  If no background color was set, transparent
338     * is returned.
339     */
340    public int getBackgroundColor() {
341        return mBackgroundColor;
342    }
343
344    /**
345     * Sets the layout style to be large or small. This affects the height of
346     * the overview, including the text description. The default is large.
347     */
348    public void setStyleLarge(boolean large) {
349        mIsStyleLarge = large;
350    }
351
352    /**
353     * Returns true if the layout style is large.
354     */
355    public boolean isStyleLarge() {
356        return mIsStyleLarge;
357    }
358
359    /**
360     * Set enter transition of target activity (typically a DetailActivity) to be
361     * transiting into overview row created by this presenter.  The transition will
362     * be cancelled if overview image is not loaded in the timeout period.
363     * <p>
364     * It assumes shared element passed from calling activity is an ImageView;
365     * the shared element transits to overview image on the left of detail
366     * overview row, while bounds of overview row grows and reveals text
367     * and buttons on the right.
368     * <p>
369     * The method must be invoked in target Activity's onCreate().
370     */
371    public final void setSharedElementEnterTransition(Activity activity,
372            String sharedElementName, long timeoutMs) {
373        if (mSharedElementHelper == null) {
374            mSharedElementHelper = new DetailsOverviewSharedElementHelper();
375        }
376        mSharedElementHelper.setSharedElementEnterTransition(activity, sharedElementName,
377                timeoutMs);
378    }
379
380    /**
381     * Set enter transition of target activity (typically a DetailActivity) to be
382     * transiting into overview row created by this presenter.  The transition will
383     * be cancelled if overview image is not loaded in a default timeout period.
384     * <p>
385     * It assumes shared element passed from calling activity is an ImageView;
386     * the shared element transits to overview image on the left of detail
387     * overview row, while bounds of overview row grows and reveals text
388     * and buttons on the right.
389     * <p>
390     * The method must be invoked in target Activity's onCreate().
391     */
392    public final void setSharedElementEnterTransition(Activity activity,
393            String sharedElementName) {
394        setSharedElementEnterTransition(activity, sharedElementName, DEFAULT_TIMEOUT);
395    }
396
397    private int getDefaultBackgroundColor(Context context) {
398        TypedValue outValue = new TypedValue();
399        context.getTheme().resolveAttribute(R.attr.defaultBrandColor, outValue, true);
400        return context.getResources().getColor(outValue.resourceId);
401    }
402
403    @Override
404    protected void onRowViewSelected(RowPresenter.ViewHolder vh, boolean selected) {
405        super.onRowViewSelected(vh, selected);
406        if (selected) {
407            ((ViewHolder) vh).dispatchItemSelection(null);
408        }
409    }
410
411    @Override
412    protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
413        View v = LayoutInflater.from(parent.getContext())
414            .inflate(R.layout.lb_details_overview, parent, false);
415        ViewHolder vh = new ViewHolder(v, mDetailsPresenter);
416
417        initDetailsOverview(vh);
418
419        return vh;
420    }
421
422    private int getCardHeight(Context context) {
423        int resId = mIsStyleLarge ? R.dimen.lb_details_overview_height_large :
424            R.dimen.lb_details_overview_height_small;
425        return context.getResources().getDimensionPixelSize(resId);
426    }
427
428    private void initDetailsOverview(ViewHolder vh) {
429        vh.mActionBridgeAdapter = new ActionsItemBridgeAdapter(vh);
430        final View overview = vh.mOverviewFrame;
431        ViewGroup.LayoutParams lp = overview.getLayoutParams();
432        lp.height = getCardHeight(overview.getContext());
433        overview.setLayoutParams(lp);
434
435        if (!getSelectEffectEnabled()) {
436            vh.mOverviewFrame.setForeground(null);
437        }
438    }
439
440    private static int getNonNegativeWidth(Drawable drawable) {
441        final int width = (drawable == null) ? 0 : drawable.getIntrinsicWidth();
442        return (width > 0 ? width : 0);
443    }
444
445    private static int getNonNegativeHeight(Drawable drawable) {
446        final int height = (drawable == null) ? 0 : drawable.getIntrinsicHeight();
447        return (height > 0 ? height : 0);
448    }
449
450    private void bindImageDrawable(ViewHolder vh) {
451        DetailsOverviewRow row = (DetailsOverviewRow) vh.getRow();
452
453        ViewGroup.MarginLayoutParams layoutParams =
454                (ViewGroup.MarginLayoutParams) vh.mImageView.getLayoutParams();
455        final int cardHeight = getCardHeight(vh.mImageView.getContext());
456        final int verticalMargin = vh.mImageView.getResources().getDimensionPixelSize(
457                R.dimen.lb_details_overview_image_margin_vertical);
458        final int horizontalMargin = vh.mImageView.getResources().getDimensionPixelSize(
459                R.dimen.lb_details_overview_image_margin_horizontal);
460        final int drawableWidth = getNonNegativeWidth(row.getImageDrawable());
461        final int drawableHeight = getNonNegativeHeight(row.getImageDrawable());
462
463        boolean scaleImage = row.isImageScaleUpAllowed();
464        boolean useMargin = false;
465
466        if (row.getImageDrawable() != null) {
467            boolean landscape = false;
468
469            // If large style and landscape image we always use margin.
470            if (drawableWidth > drawableHeight) {
471                landscape = true;
472                if (mIsStyleLarge) {
473                    useMargin = true;
474                }
475            }
476            // If long dimension bigger than the card height we scale down.
477            if ((landscape && drawableWidth > cardHeight) ||
478                    (!landscape && drawableHeight > cardHeight)) {
479                scaleImage = true;
480            }
481            // If we're not scaling to fit the card height then we always use margin.
482            if (!scaleImage) {
483                useMargin = true;
484            }
485            // If using margin than may need to scale down.
486            if (useMargin && !scaleImage) {
487                if (landscape && drawableWidth > cardHeight - horizontalMargin) {
488                    scaleImage = true;
489                } else if (!landscape && drawableHeight > cardHeight - 2 * verticalMargin) {
490                    scaleImage = true;
491                }
492            }
493        }
494
495        final int bgColor = mBackgroundColorSet ? mBackgroundColor :
496            getDefaultBackgroundColor(vh.mOverviewView.getContext());
497
498        if (useMargin) {
499            layoutParams.setMarginStart(horizontalMargin);
500            layoutParams.topMargin = layoutParams.bottomMargin = verticalMargin;
501            RoundedRectHelper.getInstance().setRoundedRectBackground(vh.mOverviewFrame, bgColor);
502            vh.mRightPanel.setBackground(null);
503            vh.mImageView.setBackground(null);
504        } else {
505            layoutParams.leftMargin = layoutParams.topMargin = layoutParams.bottomMargin = 0;
506            vh.mRightPanel.setBackgroundColor(bgColor);
507            vh.mImageView.setBackgroundColor(bgColor);
508            RoundedRectHelper.getInstance().setRoundedRectBackground(vh.mOverviewFrame,
509                    Color.TRANSPARENT);
510        }
511        if (scaleImage) {
512            vh.mImageView.setScaleType(ImageView.ScaleType.FIT_START);
513            vh.mImageView.setAdjustViewBounds(true);
514            vh.mImageView.setMaxWidth(cardHeight);
515            layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
516            layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
517        } else {
518            vh.mImageView.setScaleType(ImageView.ScaleType.CENTER);
519            vh.mImageView.setAdjustViewBounds(false);
520            layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
521            // Limit width to the card height
522            layoutParams.width = Math.min(cardHeight, drawableWidth);
523        }
524        vh.mImageView.setLayoutParams(layoutParams);
525        vh.mImageView.setImageDrawable(row.getImageDrawable());
526        if (row.getImageDrawable() != null && mSharedElementHelper != null) {
527            mSharedElementHelper.onBindToDrawable(vh);
528        }
529    }
530
531    @Override
532    protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
533        super.onBindRowViewHolder(holder, item);
534
535        DetailsOverviewRow row = (DetailsOverviewRow) item;
536        ViewHolder vh = (ViewHolder) holder;
537
538        bindImageDrawable(vh);
539        mDetailsPresenter.onBindViewHolder(vh.mDetailsDescriptionViewHolder, row.getItem());
540        vh.bindActions(row.getActionsAdapter());
541        row.addListener(vh.mListener);
542    }
543
544    @Override
545    protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) {
546        ViewHolder vh = (ViewHolder) holder;
547        DetailsOverviewRow dor = (DetailsOverviewRow) vh.getRow();
548        dor.removeListener(vh.mListener);
549        if (vh.mDetailsDescriptionViewHolder != null) {
550            mDetailsPresenter.onUnbindViewHolder(vh.mDetailsDescriptionViewHolder);
551        }
552        super.onUnbindRowViewHolder(holder);
553    }
554
555    @Override
556    public final boolean isUsingDefaultSelectEffect() {
557        return false;
558    }
559
560    @Override
561    protected void onSelectLevelChanged(RowPresenter.ViewHolder holder) {
562        super.onSelectLevelChanged(holder);
563        if (getSelectEffectEnabled()) {
564            ViewHolder vh = (ViewHolder) holder;
565            int dimmedColor = vh.mColorDimmer.getPaint().getColor();
566            ((ColorDrawable) vh.mOverviewFrame.getForeground().mutate()).setColor(dimmedColor);
567        }
568    }
569
570    @Override
571    protected void onRowViewAttachedToWindow(RowPresenter.ViewHolder vh) {
572        super.onRowViewAttachedToWindow(vh);
573        if (mDetailsPresenter != null) {
574            mDetailsPresenter.onViewAttachedToWindow(
575                    ((ViewHolder) vh).mDetailsDescriptionViewHolder);
576        }
577    }
578
579    @Override
580    protected void onRowViewDetachedFromWindow(RowPresenter.ViewHolder vh) {
581        super.onRowViewDetachedFromWindow(vh);
582        if (mDetailsPresenter != null) {
583            mDetailsPresenter.onViewDetachedFromWindow(
584                    ((ViewHolder) vh).mDetailsDescriptionViewHolder);
585        }
586    }
587}
588