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