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