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