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