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