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