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