ListRowPresenter.java revision 575dc8a5a5c023aee0c0ec297a7d357685cf49e9
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.content.Context;
17import android.content.res.TypedArray;
18import android.support.v17.leanback.R;
19import android.support.v17.leanback.graphics.ColorOverlayDimmer;
20import android.support.v17.leanback.widget.RowPresenter.ViewHolder;
21import android.util.Log;
22import android.view.View;
23import android.view.ViewGroup;
24import android.view.ViewGroup.LayoutParams;
25
26import java.util.HashMap;
27
28/**
29 * ListRowPresenter renders {@link ListRow} using a
30 * {@link HorizontalGridView} hosted in a {@link ListRowView}.
31 *
32 * <h3>Hover card</h3>
33 * Optionally, {@link #setHoverCardPresenterSelector(PresenterSelector)} can be used to
34 * display a view for the currently focused list item below the rendered
35 * list. This view is known as a hover card.
36 *
37 * <h3>Selection animation</h3>
38 * ListRowPresenter disables {@link RowPresenter}'s default dimming effect and draw
39 * a dim overlay on top of each individual child items.  Subclass may override and disable
40 * {@link #isUsingDefaultListSelectEffect()} and write its own dim effect in
41 * {@link #onSelectLevelChanged(RowPresenter.ViewHolder)}.
42 *
43 * <h3>Shadow</h3>
44 * ListRowPresenter applies a default shadow to child of each view.  Call
45 * {@link #setShadowEnabled(boolean)} to disable shadow.  Subclass may override and return
46 * false in {@link #isUsingDefaultShadow()} and replace with its own shadow implementation.
47 */
48public class ListRowPresenter extends RowPresenter {
49
50    private static final String TAG = "ListRowPresenter";
51    private static final boolean DEBUG = false;
52
53    private static final int DEFAULT_RECYCLED_POOL_SIZE = 24;
54
55    public static class ViewHolder extends RowPresenter.ViewHolder {
56        final ListRowPresenter mListRowPresenter;
57        final HorizontalGridView mGridView;
58        final ItemBridgeAdapter mItemBridgeAdapter = new ItemBridgeAdapter();
59        final HorizontalHoverCardSwitcher mHoverCardViewSwitcher = new HorizontalHoverCardSwitcher();
60        final int mPaddingTop;
61        final int mPaddingBottom;
62        final int mPaddingLeft;
63        final int mPaddingRight;
64
65        public ViewHolder(View rootView, HorizontalGridView gridView, ListRowPresenter p) {
66            super(rootView);
67            mGridView = gridView;
68            mListRowPresenter = p;
69            mPaddingTop = mGridView.getPaddingTop();
70            mPaddingBottom = mGridView.getPaddingBottom();
71            mPaddingLeft = mGridView.getPaddingLeft();
72            mPaddingRight = mGridView.getPaddingRight();
73        }
74
75        public final ListRowPresenter getListRowPresenter() {
76            return mListRowPresenter;
77        }
78
79        public final HorizontalGridView getGridView() {
80            return mGridView;
81        }
82
83        public final ItemBridgeAdapter getBridgeAdapter() {
84            return mItemBridgeAdapter;
85        }
86    }
87
88    private int mRowHeight;
89    private int mExpandedRowHeight;
90    private PresenterSelector mHoverCardPresenterSelector;
91    private int mZoomFactor;
92    private boolean mShadowEnabled = true;
93    private int mBrowseRowsFadingEdgeLength = -1;
94    private boolean mRoundedCornersEnabled = true;
95    private HashMap<Presenter, Integer> mRecycledPoolSize = new HashMap<Presenter, Integer>();
96
97    private static int sSelectedRowTopPadding;
98    private static int sExpandedSelectedRowTopPadding;
99    private static int sExpandedRowNoHovercardBottomPadding;
100
101    /**
102     * Constructs a ListRowPresenter with defaults.
103     * Uses {@link FocusHighlight#ZOOM_FACTOR_MEDIUM} for focus zooming.
104     */
105    public ListRowPresenter() {
106        this(FocusHighlight.ZOOM_FACTOR_MEDIUM);
107    }
108
109    /**
110     * Constructs a ListRowPresenter with the given parameters.
111     *
112     * @param zoomFactor Controls the zoom factor used when an item view is focused. One of
113     *         {@link FocusHighlight#ZOOM_FACTOR_NONE},
114     *         {@link FocusHighlight#ZOOM_FACTOR_SMALL},
115     *         {@link FocusHighlight#ZOOM_FACTOR_XSMALL},
116     *         {@link FocusHighlight#ZOOM_FACTOR_MEDIUM},
117     *         {@link FocusHighlight#ZOOM_FACTOR_LARGE}
118     */
119    public ListRowPresenter(int zoomFactor) {
120        if (!FocusHighlightHelper.isValidZoomIndex(zoomFactor)) {
121            throw new IllegalArgumentException("Unhandled zoom factor");
122        }
123        mZoomFactor = zoomFactor;
124    }
125
126    /**
127     * Sets the row height for rows created by this Presenter. Rows
128     * created before calling this method will not be updated.
129     *
130     * @param rowHeight Row height in pixels, or WRAP_CONTENT, or 0
131     * to use the default height.
132     */
133    public void setRowHeight(int rowHeight) {
134        mRowHeight = rowHeight;
135    }
136
137    /**
138     * Returns the row height for list rows created by this Presenter.
139     */
140    public int getRowHeight() {
141        return mRowHeight;
142    }
143
144    /**
145     * Sets the expanded row height for rows created by this Presenter.
146     * If not set, expanded rows have the same height as unexpanded
147     * rows.
148     *
149     * @param rowHeight The row height in to use when the row is expanded,
150     *        in pixels, or WRAP_CONTENT, or 0 to use the default.
151     */
152    public void setExpandedRowHeight(int rowHeight) {
153        mExpandedRowHeight = rowHeight;
154    }
155
156    /**
157     * Returns the expanded row height for rows created by this Presenter.
158     */
159    public int getExpandedRowHeight() {
160        return mExpandedRowHeight != 0 ? mExpandedRowHeight : mRowHeight;
161    }
162
163    /**
164     * Returns the zoom factor used for focus highlighting.
165     */
166    public final int getZoomFactor() {
167        return mZoomFactor;
168    }
169
170    private ItemBridgeAdapter.Wrapper mCardWrapper = new ItemBridgeAdapter.Wrapper() {
171        @Override
172        public View createWrapper(View root) {
173            ShadowOverlayContainer wrapper = new ShadowOverlayContainer(root.getContext());
174            wrapper.setLayoutParams(
175                    new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
176            wrapper.initialize(needsDefaultShadow(),
177                    needsDefaultListSelectEffect(),
178                    areChildRoundedCornersEnabled());
179            return wrapper;
180        }
181        @Override
182        public void wrap(View wrapper, View wrapped) {
183            ((ShadowOverlayContainer) wrapper).wrap(wrapped);
184        }
185    };
186
187    @Override
188    protected void initializeRowViewHolder(RowPresenter.ViewHolder holder) {
189        super.initializeRowViewHolder(holder);
190        final ViewHolder rowViewHolder = (ViewHolder) holder;
191        if (needsDefaultListSelectEffect() || needsDefaultShadow()
192                || areChildRoundedCornersEnabled()) {
193            rowViewHolder.mItemBridgeAdapter.setWrapper(mCardWrapper);
194        }
195        if (needsDefaultListSelectEffect()) {
196            ShadowOverlayContainer.prepareParentForShadow(rowViewHolder.mGridView);
197        }
198        FocusHighlightHelper.setupBrowseItemFocusHighlight(rowViewHolder.mItemBridgeAdapter,
199                mZoomFactor, false);
200        rowViewHolder.mGridView.setFocusDrawingOrderEnabled(!isUsingZOrder());
201        rowViewHolder.mGridView.setOnChildSelectedListener(
202                new OnChildSelectedListener() {
203            @Override
204            public void onChildSelected(ViewGroup parent, View view, int position, long id) {
205                selectChildView(rowViewHolder, view);
206            }
207        });
208        rowViewHolder.mItemBridgeAdapter.setAdapterListener(
209                new ItemBridgeAdapter.AdapterListener() {
210            @Override
211            public void onBind(final ItemBridgeAdapter.ViewHolder viewHolder) {
212                // Only when having an OnItemClickListner, we will attach the OnClickListener.
213                if (getOnItemClickedListener() != null || getOnItemViewClickedListener() != null) {
214                    viewHolder.mHolder.view.setOnClickListener(new View.OnClickListener() {
215                        @Override
216                        public void onClick(View v) {
217                            ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
218                                    rowViewHolder.mGridView
219                                            .getChildViewHolder(viewHolder.itemView);
220                            if (getOnItemClickedListener() != null) {
221                                getOnItemClickedListener().onItemClicked(ibh.mItem,
222                                        (ListRow) rowViewHolder.mRow);
223                            }
224                            if (getOnItemViewClickedListener() != null) {
225                                getOnItemViewClickedListener().onItemClicked(viewHolder.mHolder,
226                                        ibh.mItem, rowViewHolder, (ListRow) rowViewHolder.mRow);
227                            }
228                        }
229                    });
230                }
231            }
232
233            @Override
234            public void onUnbind(ItemBridgeAdapter.ViewHolder viewHolder) {
235                if (getOnItemClickedListener() != null || getOnItemViewClickedListener() != null) {
236                    viewHolder.mHolder.view.setOnClickListener(null);
237                }
238            }
239
240            @Override
241            public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
242                if (viewHolder.itemView instanceof ShadowOverlayContainer) {
243                    int dimmedColor = rowViewHolder.mColorDimmer.getPaint().getColor();
244                    ((ShadowOverlayContainer) viewHolder.itemView).setOverlayColor(dimmedColor);
245                }
246                rowViewHolder.syncActivatedStatus(viewHolder.itemView);
247            }
248
249            @Override
250            public void onAddPresenter(Presenter presenter, int type) {
251                rowViewHolder.getGridView().getRecycledViewPool().setMaxRecycledViews(
252                        type, getRecycledPoolSize(presenter));
253            }
254        });
255    }
256
257    final boolean needsDefaultListSelectEffect() {
258        return isUsingDefaultListSelectEffect() && getSelectEffectEnabled();
259    }
260
261    /**
262     * Sets the recycled pool size for the given presenter.
263     */
264    public void setRecycledPoolSize(Presenter presenter, int size) {
265        mRecycledPoolSize.put(presenter, size);
266    }
267
268    /**
269     * Returns the recycled pool size for the given presenter.
270     */
271    public int getRecycledPoolSize(Presenter presenter) {
272        return mRecycledPoolSize.containsKey(presenter) ? mRecycledPoolSize.get(presenter) :
273                DEFAULT_RECYCLED_POOL_SIZE;
274    }
275
276    /**
277     * Set {@link PresenterSelector} used for showing a select object in a hover card.
278     */
279    public final void setHoverCardPresenterSelector(PresenterSelector selector) {
280        mHoverCardPresenterSelector = selector;
281    }
282
283    /**
284     * Get {@link PresenterSelector} used for showing a select object in a hover card.
285     */
286    public final PresenterSelector getHoverCardPresenterSelector() {
287        return mHoverCardPresenterSelector;
288    }
289
290    /*
291     * Perform operations when a child of horizontal grid view is selected.
292     */
293    private void selectChildView(ViewHolder rowViewHolder, View view) {
294        ItemBridgeAdapter.ViewHolder ibh = null;
295        if (view != null) {
296            ibh = (ItemBridgeAdapter.ViewHolder)
297                    rowViewHolder.mGridView.getChildViewHolder(view);
298        }
299        if (view == null) {
300            if (mHoverCardPresenterSelector != null) {
301                rowViewHolder.mHoverCardViewSwitcher.unselect();
302            }
303            if (getOnItemViewSelectedListener() != null) {
304                getOnItemViewSelectedListener().onItemSelected(null, null,
305                        rowViewHolder, rowViewHolder.mRow);
306            }
307            if (getOnItemSelectedListener() != null) {
308                getOnItemSelectedListener().onItemSelected(null, rowViewHolder.mRow);
309            }
310        } else if (rowViewHolder.mExpanded && rowViewHolder.mSelected) {
311            if (mHoverCardPresenterSelector != null) {
312                rowViewHolder.mHoverCardViewSwitcher.select(rowViewHolder.mGridView, view,
313                        ibh.mItem);
314            }
315            if (getOnItemViewSelectedListener() != null) {
316                getOnItemViewSelectedListener().onItemSelected(ibh.mHolder, ibh.mItem,
317                        rowViewHolder, rowViewHolder.mRow);
318            }
319            if (getOnItemSelectedListener() != null) {
320                getOnItemSelectedListener().onItemSelected(ibh.mItem, rowViewHolder.mRow);
321            }
322        }
323    }
324
325    private static void initStatics(Context context) {
326        if (sSelectedRowTopPadding == 0) {
327            sSelectedRowTopPadding = context.getResources().getDimensionPixelSize(
328                    R.dimen.lb_browse_selected_row_top_padding);
329            sExpandedSelectedRowTopPadding = context.getResources().getDimensionPixelSize(
330                    R.dimen.lb_browse_expanded_selected_row_top_padding);
331            sExpandedRowNoHovercardBottomPadding = context.getResources().getDimensionPixelSize(
332                    R.dimen.lb_browse_expanded_row_no_hovercard_bottom_padding);
333        }
334    }
335
336    private int getSpaceUnderBaseline(ListRowPresenter.ViewHolder vh) {
337        RowHeaderPresenter.ViewHolder headerViewHolder = vh.getHeaderViewHolder();
338        if (headerViewHolder != null) {
339            if (getHeaderPresenter() != null) {
340                return getHeaderPresenter().getSpaceUnderBaseline(headerViewHolder);
341            }
342            return headerViewHolder.view.getPaddingBottom();
343        }
344        return 0;
345    }
346
347    private void setVerticalPadding(ListRowPresenter.ViewHolder vh) {
348        int paddingTop, paddingBottom;
349        // Note: sufficient bottom padding needed for card shadows.
350        if (vh.isExpanded()) {
351            int headerSpaceUnderBaseline = getSpaceUnderBaseline(vh);
352            if (DEBUG) Log.v(TAG, "headerSpaceUnderBaseline " + headerSpaceUnderBaseline);
353            paddingTop = (vh.isSelected() ? sExpandedSelectedRowTopPadding : vh.mPaddingTop) -
354                    headerSpaceUnderBaseline;
355            paddingBottom = mHoverCardPresenterSelector == null ?
356                    sExpandedRowNoHovercardBottomPadding : vh.mPaddingBottom;
357        } else if (vh.isSelected()) {
358            paddingTop = sSelectedRowTopPadding - vh.mPaddingBottom;
359            paddingBottom = sSelectedRowTopPadding;
360        } else {
361            paddingTop = 0;
362            paddingBottom = vh.mPaddingBottom;
363        }
364        vh.getGridView().setPadding(vh.mPaddingLeft, paddingTop, vh.mPaddingRight,
365                paddingBottom);
366    }
367
368    @Override
369    protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
370        initStatics(parent.getContext());
371        ListRowView rowView = new ListRowView(parent.getContext());
372        setupFadingEffect(rowView);
373        if (mRowHeight != 0) {
374            rowView.getGridView().setRowHeight(mRowHeight);
375        }
376        return new ViewHolder(rowView, rowView.getGridView(), this);
377    }
378
379    @Override
380    protected void onRowViewSelected(RowPresenter.ViewHolder holder, boolean selected) {
381        super.onRowViewSelected(holder, selected);
382        ViewHolder vh = (ViewHolder) holder;
383        setVerticalPadding(vh);
384        updateFooterViewSwitcher(vh);
385    }
386
387    /*
388     * Show or hide hover card when row selection or expanded state is changed.
389     */
390    private void updateFooterViewSwitcher(ViewHolder vh) {
391        if (vh.mExpanded && vh.mSelected) {
392            if (mHoverCardPresenterSelector != null) {
393                vh.mHoverCardViewSwitcher.init((ViewGroup) vh.view,
394                        mHoverCardPresenterSelector);
395            }
396            ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
397                    vh.mGridView.findViewHolderForPosition(
398                            vh.mGridView.getSelectedPosition());
399            selectChildView(vh, ibh == null ? null : ibh.itemView);
400        } else {
401            if (mHoverCardPresenterSelector != null) {
402                vh.mHoverCardViewSwitcher.unselect();
403            }
404        }
405    }
406
407    private void setupFadingEffect(ListRowView rowView) {
408        // content is completely faded at 1/2 padding of left, fading length is 1/2 of padding.
409        HorizontalGridView gridView = rowView.getGridView();
410        if (mBrowseRowsFadingEdgeLength < 0) {
411            TypedArray ta = gridView.getContext()
412                    .obtainStyledAttributes(R.styleable.LeanbackTheme);
413            mBrowseRowsFadingEdgeLength = (int) ta.getDimension(
414                    R.styleable.LeanbackTheme_browseRowsFadingEdgeLength, 0);
415            ta.recycle();
416        }
417        gridView.setFadingLeftEdgeLength(mBrowseRowsFadingEdgeLength);
418    }
419
420    @Override
421    protected void onRowViewExpanded(RowPresenter.ViewHolder holder, boolean expanded) {
422        super.onRowViewExpanded(holder, expanded);
423        ViewHolder vh = (ViewHolder) holder;
424        if (getRowHeight() != getExpandedRowHeight()) {
425            int newHeight = expanded ? getExpandedRowHeight() : getRowHeight();
426            vh.getGridView().setRowHeight(newHeight);
427        }
428        setVerticalPadding(vh);
429        updateFooterViewSwitcher(vh);
430    }
431
432    @Override
433    protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
434        super.onBindRowViewHolder(holder, item);
435        ViewHolder vh = (ViewHolder) holder;
436        ListRow rowItem = (ListRow) item;
437        vh.mItemBridgeAdapter.setAdapter(rowItem.getAdapter());
438        vh.mGridView.setAdapter(vh.mItemBridgeAdapter);
439    }
440
441    @Override
442    protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) {
443        ViewHolder vh = (ViewHolder) holder;
444        vh.mGridView.setAdapter(null);
445        vh.mItemBridgeAdapter.clear();
446        super.onUnbindRowViewHolder(holder);
447    }
448
449    /**
450     * ListRowPresenter overrides the default select effect of {@link RowPresenter}
451     * and return false.
452     */
453    @Override
454    public final boolean isUsingDefaultSelectEffect() {
455        return false;
456    }
457
458    /**
459     * Returns true so that default select effect is applied to each individual
460     * child of {@link HorizontalGridView}.  Subclass may return false to disable
461     * the default implementation.
462     * @see #onSelectLevelChanged(RowPresenter.ViewHolder)
463     */
464    public boolean isUsingDefaultListSelectEffect() {
465        return true;
466    }
467
468    /**
469     * Returns true if SDK >= 18, where default shadow
470     * is applied to each individual child of {@link HorizontalGridView}.
471     * Subclass may return false to disable.
472     */
473    public boolean isUsingDefaultShadow() {
474        return ShadowOverlayContainer.supportsShadow();
475    }
476
477    /**
478     * Returns true if SDK >= L, where Z shadow is enabled so that Z order is enabled
479     * on each child of horizontal list.   If subclass returns false in isUsingDefaultShadow()
480     * and does not use Z-shadow on SDK >= L, it should override isUsingZOrder() return false.
481     */
482    public boolean isUsingZOrder() {
483        return ShadowHelper.getInstance().usesZShadow();
484    }
485
486    /**
487     * Enable or disable child shadow.
488     * This is not only for enable/disable default shadow implementation but also subclass must
489     * respect this flag.
490     */
491    public final void setShadowEnabled(boolean enabled) {
492        mShadowEnabled = enabled;
493    }
494
495    /**
496     * Returns true if child shadow is enabled.
497     * This is not only for enable/disable default shadow implementation but also subclass must
498     * respect this flag.
499     */
500    public final boolean getShadowEnabled() {
501        return mShadowEnabled;
502    }
503
504    /**
505     * Enables or disabled rounded corners on children of this row.
506     * Supported on Android SDK >= L.
507     */
508    public final void enableChildRoundedCorners(boolean enable) {
509        mRoundedCornersEnabled = enable;
510    }
511
512    /**
513     * Returns true if rounded corners are enabled for children of this row.
514     */
515    public final boolean areChildRoundedCornersEnabled() {
516        return mRoundedCornersEnabled;
517    }
518
519    final boolean needsDefaultShadow() {
520        return isUsingDefaultShadow() && getShadowEnabled();
521    }
522
523    @Override
524    public boolean canDrawOutOfBounds() {
525        return needsDefaultShadow();
526    }
527
528    /**
529     * Applies select level to header and draw a default color dim over each child
530     * of {@link HorizontalGridView}.
531     * <p>
532     * Subclass may override this method.  A subclass
533     * needs to call super.onSelectLevelChanged() for applying header select level
534     * and optionally applying a default select level to each child view of
535     * {@link HorizontalGridView} if {@link #isUsingDefaultListSelectEffect()}
536     * is true.  Subclass may override {@link #isUsingDefaultListSelectEffect()} to return
537     * false and deal with the individual item select level by itself.
538     * </p>
539     */
540    @Override
541    protected void onSelectLevelChanged(RowPresenter.ViewHolder holder) {
542        super.onSelectLevelChanged(holder);
543        if (needsDefaultListSelectEffect()) {
544            ViewHolder vh = (ViewHolder) holder;
545            int dimmedColor = vh.mColorDimmer.getPaint().getColor();
546            for (int i = 0, count = vh.mGridView.getChildCount(); i < count; i++) {
547                ShadowOverlayContainer wrapper = (ShadowOverlayContainer) vh.mGridView.getChildAt(i);
548                wrapper.setOverlayColor(dimmedColor);
549            }
550            if (vh.mGridView.getFadingLeftEdge()) {
551                vh.mGridView.invalidate();
552            }
553        }
554    }
555
556    @Override
557    public void freeze(RowPresenter.ViewHolder holder, boolean freeze) {
558        ViewHolder vh = (ViewHolder) holder;
559        vh.mGridView.setScrollEnabled(!freeze);
560    }
561
562    @Override
563    public void setEntranceTransitionState(RowPresenter.ViewHolder holder,
564            boolean afterEntrance) {
565        super.setEntranceTransitionState(holder, afterEntrance);
566        ((ViewHolder) holder).mGridView.setChildrenVisibility(
567                afterEntrance? View.VISIBLE : View.INVISIBLE);
568    }
569}
570