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