ListRowPresenter.java revision 0fcad32410ad29031630bb9cc2da35d33a4aa906
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 ColorOverlayDimmer mColorDimmer;
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            mColorDimmer = ColorOverlayDimmer.createDefault(rootView.getContext());
66            mPaddingTop = mGridView.getPaddingTop();
67            mPaddingBottom = mGridView.getPaddingBottom();
68            mPaddingLeft = mGridView.getPaddingLeft();
69            mPaddingRight = mGridView.getPaddingRight();
70        }
71
72        public final ListRowPresenter getListRowPresenter() {
73            return mListRowPresenter;
74        }
75
76        public final HorizontalGridView getGridView() {
77            return mGridView;
78        }
79
80        public final ItemBridgeAdapter getBridgeAdapter() {
81            return mItemBridgeAdapter;
82        }
83    }
84
85    private int mRowHeight;
86    private int mExpandedRowHeight;
87    private PresenterSelector mHoverCardPresenterSelector;
88    private int mZoomFactor;
89    private boolean mShadowEnabled = true;
90    private int mBrowseRowsFadingEdgeLength = -1;
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(), needsDefaultListSelectEffect());
168            return wrapper;
169        }
170        @Override
171        public void wrap(View wrapper, View wrapped) {
172            ((ShadowOverlayContainer) wrapper).wrap(wrapped);
173        }
174    };
175
176    @Override
177    protected void initializeRowViewHolder(RowPresenter.ViewHolder holder) {
178        super.initializeRowViewHolder(holder);
179        final ViewHolder rowViewHolder = (ViewHolder) holder;
180        if (needsDefaultListSelectEffect() || needsDefaultShadow()) {
181            rowViewHolder.mItemBridgeAdapter.setWrapper(mCardWrapper);
182        }
183        if (needsDefaultListSelectEffect()) {
184            ShadowOverlayContainer.prepareParentForShadow(rowViewHolder.mGridView);
185            ((ViewGroup) rowViewHolder.view).setClipChildren(false);
186            if (rowViewHolder.mContainerViewHolder != null) {
187                ((ViewGroup) rowViewHolder.mContainerViewHolder.view).setClipChildren(false);
188            }
189        }
190        FocusHighlightHelper.setupBrowseItemFocusHighlight(rowViewHolder.mItemBridgeAdapter, mZoomFactor);
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 onCreate(final ItemBridgeAdapter.ViewHolder viewHolder) {
203                // Only when having an OnItemClickListner, we will attach the OnClickListener.
204                if (getOnItemClickedListener() != 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.getChildViewHolder(viewHolder.itemView);
210                            if (getOnItemClickedListener() != null) {
211                                getOnItemClickedListener().onItemClicked(ibh.mItem,
212                                        (ListRow) rowViewHolder.mRow);
213                            }
214                        }
215                    });
216                }
217            }
218
219            @Override
220            public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
221                if (viewHolder.itemView instanceof ShadowOverlayContainer) {
222                    int dimmedColor = rowViewHolder.mColorDimmer.getPaint().getColor();
223                    ((ShadowOverlayContainer) viewHolder.itemView).setOverlayColor(dimmedColor);
224                }
225                viewHolder.itemView.setActivated(rowViewHolder.mExpanded);
226            }
227
228            @Override
229            public void onAddPresenter(Presenter presenter, int type) {
230                rowViewHolder.getGridView().getRecycledViewPool().setMaxRecycledViews(type, 24);
231            }
232        });
233    }
234
235    final boolean needsDefaultListSelectEffect() {
236        return isUsingDefaultListSelectEffect() && getSelectEffectEnabled();
237    }
238
239    /**
240     * Set {@link PresenterSelector} used for showing a select object in a hover card.
241     */
242    public final void setHoverCardPresenterSelector(PresenterSelector selector) {
243        mHoverCardPresenterSelector = selector;
244    }
245
246    /**
247     * Get {@link PresenterSelector} used for showing a select object in a hover card.
248     */
249    public final PresenterSelector getHoverCardPresenterSelector() {
250        return mHoverCardPresenterSelector;
251    }
252
253    /*
254     * Perform operations when a child of horizontal grid view is selected.
255     */
256    private void selectChildView(ViewHolder rowViewHolder, View view) {
257        ItemBridgeAdapter.ViewHolder ibh = null;
258        if (view != null) {
259            ibh = (ItemBridgeAdapter.ViewHolder)
260                    rowViewHolder.mGridView.getChildViewHolder(view);
261        }
262        if (view == null) {
263            if (mHoverCardPresenterSelector != null) {
264                rowViewHolder.mHoverCardViewSwitcher.unselect();
265            }
266            if (getOnItemSelectedListener() != null) {
267                getOnItemSelectedListener().onItemSelected(null, rowViewHolder.mRow);
268            }
269        } else if (rowViewHolder.mExpanded && rowViewHolder.mSelected) {
270            if (mHoverCardPresenterSelector != null) {
271                rowViewHolder.mHoverCardViewSwitcher.select(rowViewHolder.mGridView, view,
272                        ibh.mItem);
273            }
274            if (getOnItemSelectedListener() != null) {
275                getOnItemSelectedListener().onItemSelected(ibh.mItem, rowViewHolder.mRow);
276            }
277        }
278    }
279
280    private static void initStatics(Context context) {
281        if (sSelectedRowTopPadding == 0) {
282            sSelectedRowTopPadding = context.getResources().getDimensionPixelSize(
283                    R.dimen.lb_browse_selected_row_top_padding);
284            sExpandedSelectedRowTopPadding = context.getResources().getDimensionPixelSize(
285                    R.dimen.lb_browse_expanded_selected_row_top_padding);
286            sExpandedRowNoHovercardBottomPadding = context.getResources().getDimensionPixelSize(
287                    R.dimen.lb_browse_expanded_row_no_hovercard_bottom_padding);
288        }
289    }
290
291    private int getSpaceUnderBaseline(ListRowPresenter.ViewHolder vh) {
292        RowHeaderPresenter.ViewHolder headerViewHolder = vh.getHeaderViewHolder();
293        if (headerViewHolder != null) {
294            if (getHeaderPresenter() != null) {
295                return getHeaderPresenter().getSpaceUnderBaseline(headerViewHolder);
296            }
297            return headerViewHolder.view.getPaddingBottom();
298        }
299        return 0;
300    }
301
302    private void setVerticalPadding(ListRowPresenter.ViewHolder vh) {
303        int paddingTop, paddingBottom;
304        if (vh.isExpanded()) {
305            int headerSpaceUnderBaseline = getSpaceUnderBaseline(vh);
306            if (DEBUG) Log.v(TAG, "headerSpaceUnderBaseline " + headerSpaceUnderBaseline);
307            paddingTop = (vh.isSelected() ? sExpandedSelectedRowTopPadding : vh.mPaddingTop) -
308                    headerSpaceUnderBaseline;
309            paddingBottom = mHoverCardPresenterSelector == null ?
310                    sExpandedRowNoHovercardBottomPadding : vh.mPaddingBottom;
311        } else if (vh.isSelected()) {
312            paddingTop = sSelectedRowTopPadding;
313            paddingBottom = sSelectedRowTopPadding - vh.mPaddingTop;
314        } else {
315            paddingTop = vh.mPaddingTop;
316            paddingBottom = 0;
317        }
318        vh.getGridView().setPadding(vh.mPaddingLeft, paddingTop, vh.mPaddingRight,
319                paddingBottom);
320    }
321
322    @Override
323    protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
324        initStatics(parent.getContext());
325        ListRowView rowView = new ListRowView(parent.getContext());
326        setupFadingEffect(rowView);
327        if (mRowHeight != 0) {
328            rowView.getGridView().setRowHeight(mRowHeight);
329        }
330        return new ViewHolder(rowView, rowView.getGridView(), this);
331    }
332
333    @Override
334    protected void onRowViewSelected(RowPresenter.ViewHolder holder, boolean selected) {
335        super.onRowViewSelected(holder, selected);
336        ViewHolder vh = (ViewHolder) holder;
337        setVerticalPadding(vh);
338        updateFooterViewSwitcher(vh);
339    }
340
341    /*
342     * Show or hide hover card when row selection or expanded state is changed.
343     */
344    private void updateFooterViewSwitcher(ViewHolder vh) {
345        if (vh.mExpanded && vh.mSelected) {
346            if (mHoverCardPresenterSelector != null) {
347                vh.mHoverCardViewSwitcher.init((ViewGroup) vh.view,
348                        mHoverCardPresenterSelector);
349            }
350            ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
351                    vh.mGridView.findViewHolderForPosition(
352                            vh.mGridView.getSelectedPosition());
353            selectChildView(vh, ibh == null ? null : ibh.itemView);
354        } else {
355            if (mHoverCardPresenterSelector != null) {
356                vh.mHoverCardViewSwitcher.unselect();
357            }
358        }
359    }
360
361    private void setupFadingEffect(ListRowView rowView) {
362        // content is completely faded at 1/2 padding of left, fading length is 1/2 of padding.
363        HorizontalGridView gridView = rowView.getGridView();
364        if (mBrowseRowsFadingEdgeLength < 0) {
365            TypedArray ta = gridView.getContext()
366                    .obtainStyledAttributes(R.styleable.LeanbackTheme);
367            mBrowseRowsFadingEdgeLength = (int) ta.getDimension(
368                    R.styleable.LeanbackTheme_browseRowsFadingEdgeLength, 0);
369            ta.recycle();
370        }
371        gridView.setFadingLeftEdgeLength(mBrowseRowsFadingEdgeLength);
372    }
373
374    @Override
375    protected void onRowViewExpanded(RowPresenter.ViewHolder holder, boolean expanded) {
376        super.onRowViewExpanded(holder, expanded);
377        ViewHolder vh = (ViewHolder) holder;
378        if (getRowHeight() != getExpandedRowHeight()) {
379            int newHeight = expanded ? getExpandedRowHeight() : getRowHeight();
380            vh.getGridView().setRowHeight(newHeight);
381        }
382        setVerticalPadding(vh);
383        vh.getGridView().setFadingLeftEdge(!expanded);
384        updateFooterViewSwitcher(vh);
385    }
386
387    @Override
388    protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
389        super.onBindRowViewHolder(holder, item);
390        ViewHolder vh = (ViewHolder) holder;
391        ListRow rowItem = (ListRow) item;
392        vh.mItemBridgeAdapter.clear();
393        vh.mItemBridgeAdapter.setAdapter(rowItem.getAdapter());
394        vh.mGridView.setAdapter(vh.mItemBridgeAdapter);
395    }
396
397    @Override
398    protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) {
399        ((ViewHolder) holder).mGridView.setAdapter(null);
400        super.onUnbindRowViewHolder(holder);
401    }
402
403    /**
404     * ListRowPresenter overrides the default select effect of {@link RowPresenter}
405     * and return false.
406     */
407    @Override
408    public final boolean isUsingDefaultSelectEffect() {
409        return false;
410    }
411
412    /**
413     * Returns true so that default select effect is applied to each individual
414     * child of {@link HorizontalGridView}.  Subclass may return false to disable
415     * the default implementation.
416     * @see #onSelectLevelChanged(RowPresenter.ViewHolder)
417     */
418    public boolean isUsingDefaultListSelectEffect() {
419        return true;
420    }
421
422    /**
423     * Returns true if SDK >= 18, where default shadow
424     * is applied to each individual child of {@link HorizontalGridView}.
425     * Subclass may return false to disable.
426     */
427    public boolean isUsingDefaultShadow() {
428        return ShadowOverlayContainer.supportsShadow();
429    }
430
431    /**
432     * Returns true if SDK >= L, where Z shadow is enabled so that Z order is enabled
433     * on each child of horizontal list.   If subclass returns false in isUsingDefaultShadow()
434     * and does not use Z-shadow on SDK >= L, it should override isUsingZOrder() return false.
435     */
436    public boolean isUsingZOrder() {
437        return ShadowHelper.getInstance().usesZShadow();
438    }
439
440    /**
441     * Enable or disable child shadow.
442     * This is not only for enable/disable default shadow implementation but also subclass must
443     * respect this flag.
444     */
445    public final void setShadowEnabled(boolean enabled) {
446        mShadowEnabled = enabled;
447    }
448
449    /**
450     * Returns true if child shadow is enabled.
451     * This is not only for enable/disable default shadow implementation but also subclass must
452     * respect this flag.
453     */
454    public final boolean getShadowEnabled() {
455        return mShadowEnabled;
456    }
457
458    final boolean needsDefaultShadow() {
459        return isUsingDefaultShadow() && getShadowEnabled();
460    }
461
462    @Override
463    public boolean canDrawOutOfBounds() {
464        return needsDefaultShadow();
465    }
466
467    /**
468     * Applies select level to header and draw a default color dim over each child
469     * of {@link HorizontalGridView}.
470     * <p>
471     * Subclass may override this method.  A subclass
472     * needs to call super.onSelectLevelChanged() for applying header select level
473     * and optionally applying a default select level to each child view of
474     * {@link HorizontalGridView} if {@link #isUsingDefaultListSelectEffect()}
475     * is true.  Subclass may override {@link #isUsingDefaultListSelectEffect()} to return
476     * false and deal with the individual item select level by itself.
477     * </p>
478     */
479    @Override
480    protected void onSelectLevelChanged(RowPresenter.ViewHolder holder) {
481        super.onSelectLevelChanged(holder);
482        if (needsDefaultListSelectEffect()) {
483            ViewHolder vh = (ViewHolder) holder;
484            vh.mColorDimmer.setActiveLevel(holder.mSelectLevel);
485            int dimmedColor = vh.mColorDimmer.getPaint().getColor();
486            for (int i = 0, count = vh.mGridView.getChildCount(); i < count; i++) {
487                ShadowOverlayContainer wrapper = (ShadowOverlayContainer) vh.mGridView.getChildAt(i);
488                wrapper.setOverlayColor(dimmedColor);
489            }
490            if (vh.mGridView.getFadingLeftEdge()) {
491                vh.mGridView.invalidate();
492            }
493        }
494    }
495
496}
497