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