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