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