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