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