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