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