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