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