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