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