ListRowPresenter.java revision dfd01bbadc107b6b3b2081ddb0236128c425f380
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.graphics.Canvas;
20import android.support.v17.leanback.R;
21import android.support.v17.leanback.graphics.ColorOverlayDimmer;
22import android.support.v17.leanback.widget.Presenter.ViewHolder;
23import android.support.v7.widget.RecyclerView;
24import android.util.AttributeSet;
25import android.view.View;
26import android.view.ViewGroup;
27import android.view.ViewGroup.LayoutParams;
28import android.widget.FrameLayout;
29
30/**
31 * ListRowPresenter renders {@link ListRow} using a
32 * {@link HorizontalGridView} hosted in a {@link BrowseRowView}.
33 *
34 * <h3>Hover card</h3>
35 * Optionally, {@link #setHoverCardPresenterSelector(PresenterSelector)} can be used to
36 * display a view for the currently focused list item below the rendered
37 * list. This view is known as a hover card.
38 *
39 * <h3>Selection animation</h3>
40 * ListRowPresenter disables {@link RowPresenter}'s default dimming effect and draw
41 * a dim overlay on top of each individual child items.  Subclass may override and disable
42 * {@link #isUsingDefaultListSelectEffect()} and write its own dim effect in
43 * {@link #onSelectLevelChanged(RowPresenter.ViewHolder)}.
44 *
45 * <h3>Shadow</h3>
46 * ListRowPresenter applies a default shadow to child of each view.  Call
47 * {@link #setShadowEnabled(boolean)} to disable shadow.  Subclass may override and return
48 * false in {@link #isUsingDefaultShadow()} and replace with its own shadow implementation.
49 */
50public class ListRowPresenter extends RowPresenter {
51
52    private static final String TAG = "ListRowPresenter";
53    private static final boolean DEBUG = false;
54
55    /**
56     * No zoom factor.
57     * @deprecated Use {@link FocusHighlight#ZOOM_FACTOR_NONE} instead.
58     */
59    @Deprecated
60    public static final int ZOOM_FACTOR_NONE = FocusHighlight.ZOOM_FACTOR_NONE;
61
62    /**
63     * A small zoom factor, recommended for large item views.
64     * @deprecated Use {@link FocusHighlight#ZOOM_FACTOR_SMALL} instead.
65     */
66    @Deprecated
67    public static final int ZOOM_FACTOR_SMALL = FocusHighlight.ZOOM_FACTOR_SMALL;
68
69    /**
70     * A medium zoom factor, recommended for medium sized item views.
71     * @deprecated Use {@link FocusHighlight#ZOOM_FACTOR_MEDIUM} instead.
72     */
73    @Deprecated
74    public static final int ZOOM_FACTOR_MEDIUM = FocusHighlight.ZOOM_FACTOR_MEDIUM;
75
76    /**
77     * A large zoom factor, recommended for small item views.
78     * @deprecated Use {@link FocusHighlight#ZOOM_FACTOR_LARGE} instead.
79     */
80    @Deprecated
81    public static final int ZOOM_FACTOR_LARGE = FocusHighlight.ZOOM_FACTOR_LARGE;
82
83    public static class ViewHolder extends RowPresenter.ViewHolder {
84        final ListRowPresenter mListRowPresenter;
85        final HorizontalGridView mGridView;
86        final ItemBridgeAdapter mItemBridgeAdapter = new ItemBridgeAdapter();
87        final HorizontalHoverCardSwitcher mHoverCardViewSwitcher = new HorizontalHoverCardSwitcher();
88        final ColorOverlayDimmer mColorDimmer;
89
90        public ViewHolder(View rootView, HorizontalGridView gridView, ListRowPresenter p) {
91            super(rootView);
92            mGridView = gridView;
93            mListRowPresenter = p;
94            mColorDimmer = ColorOverlayDimmer.createDefault(rootView.getContext());
95        }
96
97        public final ListRowPresenter getListRowPresenter() {
98            return mListRowPresenter;
99        }
100
101        public final HorizontalGridView getGridView() {
102            return mGridView;
103        }
104    }
105
106    private PresenterSelector mHoverCardPresenterSelector;
107    private int mZoomFactor;
108    private boolean mShadowEnabled = true;
109
110    /**
111     * Constructs a ListRowPresenter with defaults.
112     * Uses {@link FocusHighlight#ZOOM_FACTOR_MEDIUM} for focus zooming.
113     */
114    public ListRowPresenter() {
115        this(FocusHighlight.ZOOM_FACTOR_MEDIUM);
116    }
117
118    /**
119     * Constructs a ListRowPresenter with the given parameters.
120     *
121     * @param zoomFactor Controls the zoom factor used when an item view is focused. One of
122     *         {@link FocusHighlight#ZOOM_FACTOR_NONE},
123     *         {@link FocusHighlight#ZOOM_FACTOR_SMALL},
124     *         {@link FocusHighlight#ZOOM_FACTOR_MEDIUM},
125     *         {@link FocusHighlight#ZOOM_FACTOR_LARGE}
126     */
127    public ListRowPresenter(int zoomFactor) {
128        mZoomFactor = zoomFactor;
129    }
130
131    /**
132     * Returns the zoom factor used for focus highlighting.
133     */
134    public final int getZoomFactor() {
135        return mZoomFactor;
136    }
137
138    private ItemBridgeAdapter.Wrapper mCardWrapper = new ItemBridgeAdapter.Wrapper() {
139        @Override
140        public View createWrapper(View root) {
141            ShadowOverlayContainer wrapper = new ShadowOverlayContainer(root.getContext());
142            wrapper.setLayoutParams(
143                    new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
144            wrapper.initialize(needsDefaultShadow(), needsDefaultSelectEffect());
145            return wrapper;
146        }
147        @Override
148        public void wrap(View wrapper, View wrapped) {
149            ((ShadowOverlayContainer) wrapper).wrap(wrapped);
150        }
151    };
152
153    @Override
154    protected void initializeRowViewHolder(RowPresenter.ViewHolder holder) {
155        super.initializeRowViewHolder(holder);
156        final ViewHolder rowViewHolder = (ViewHolder) holder;
157        if (needsDefaultSelectEffect() || needsDefaultShadow()) {
158            rowViewHolder.mItemBridgeAdapter.setWrapper(mCardWrapper);
159        }
160        if (needsDefaultShadow()) {
161            ShadowOverlayContainer.prepareParentForShadow(rowViewHolder.mGridView);
162            ((ViewGroup) rowViewHolder.view).setClipChildren(false);
163        }
164        FocusHighlightHelper.setupBrowseItemFocusHighlight(rowViewHolder.mItemBridgeAdapter, mZoomFactor);
165        rowViewHolder.mGridView.setOnChildSelectedListener(
166                new OnChildSelectedListener() {
167            @Override
168            public void onChildSelected(ViewGroup parent, View view, int position, long id) {
169                selectChildView(rowViewHolder, view);
170            }
171        });
172        rowViewHolder.mItemBridgeAdapter.setAdapterListener(
173                new ItemBridgeAdapter.AdapterListener() {
174            @Override
175            public void onCreate(final ItemBridgeAdapter.ViewHolder viewHolder) {
176                // Only when having an OnItemClickListner, we will attach the OnClickListener.
177                if (getOnItemClickedListener() != null) {
178                    viewHolder.mHolder.view.setOnClickListener(new View.OnClickListener() {
179                        @Override
180                        public void onClick(View v) {
181                            ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
182                                    rowViewHolder.mGridView.getChildViewHolder(viewHolder.itemView);
183                            if (getOnItemClickedListener() != null) {
184                                getOnItemClickedListener().onItemClicked(ibh.mItem,
185                                        (ListRow) rowViewHolder.mRow);
186                            }
187                        }
188                    });
189                }
190            }
191
192            @Override
193            public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
194                if (viewHolder.itemView instanceof ShadowOverlayContainer) {
195                    int dimmedColor = rowViewHolder.mColorDimmer.getPaint().getColor();
196                    ((ShadowOverlayContainer) viewHolder.itemView).setOverlayColor(dimmedColor);
197                }
198            }
199        });
200    }
201
202    final boolean needsDefaultSelectEffect() {
203        return isUsingDefaultListSelectEffect() && getSelectEffectEnabled();
204    }
205
206    /**
207     * Set {@link PresenterSelector} used for showing a select object in a hover card.
208     */
209    public final void setHoverCardPresenterSelector(PresenterSelector selector) {
210        mHoverCardPresenterSelector = selector;
211    }
212
213    /**
214     * Get {@link PresenterSelector} used for showing a select object in a hover card.
215     */
216    public final PresenterSelector getHoverCardPresenterSelector() {
217        return mHoverCardPresenterSelector;
218    }
219
220    /*
221     * Perform operations when a child of horizontal grid view is selected.
222     */
223    private void selectChildView(ViewHolder rowViewHolder, View view) {
224        ItemBridgeAdapter.ViewHolder ibh = null;
225        if (view != null) {
226            ibh = (ItemBridgeAdapter.ViewHolder)
227                    rowViewHolder.mGridView.getChildViewHolder(view);
228        }
229        if (view == null) {
230            if (mHoverCardPresenterSelector != null) {
231                rowViewHolder.mHoverCardViewSwitcher.unselect();
232            }
233            if (getOnItemSelectedListener() != null) {
234                getOnItemSelectedListener().onItemSelected(null, rowViewHolder.mRow);
235            }
236        } else if (rowViewHolder.mExpanded && rowViewHolder.mSelected) {
237            if (mHoverCardPresenterSelector != null) {
238                rowViewHolder.mHoverCardViewSwitcher.select(rowViewHolder.mGridView, view,
239                        ibh.mItem);
240            }
241            if (getOnItemSelectedListener() != null) {
242                getOnItemSelectedListener().onItemSelected(ibh.mItem, rowViewHolder.mRow);
243            }
244        }
245    }
246
247    @Override
248    protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
249        BrowseRowView rowView = new BrowseRowView(parent.getContext());
250        return new ViewHolder(rowView, rowView.getGridView(), this);
251    }
252
253    @Override
254    protected void onRowViewSelected(RowPresenter.ViewHolder holder, boolean selected) {
255        updateFooterViewSwitcher((ViewHolder) holder);
256        updateInitialChildSelection((ViewHolder) holder);
257    }
258
259    /*
260     * Show or hide hover card when row selection or expanded state is changed.
261     */
262    private void updateFooterViewSwitcher(ViewHolder vh) {
263        if (vh.mExpanded && vh.mSelected) {
264            if (mHoverCardPresenterSelector != null) {
265                vh.mHoverCardViewSwitcher.init((ViewGroup) vh.view,
266                        mHoverCardPresenterSelector);
267            }
268        } else {
269            if (mHoverCardPresenterSelector != null) {
270                vh.mHoverCardViewSwitcher.clear();
271            }
272        }
273    }
274
275    /*
276     * Make initial child selection when row selection state is changed.
277     */
278    private void updateInitialChildSelection(ViewHolder vh) {
279        if (vh.mExpanded && vh.mSelected) {
280            ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
281                    vh.mGridView.findViewHolderForPosition(
282                            vh.mGridView.getSelectedPosition());
283            selectChildView(vh, ibh == null ? null : ibh.itemView);
284        } else {
285            selectChildView(vh, null);
286        }
287    }
288
289    @Override
290    protected void onRowViewExpanded(RowPresenter.ViewHolder holder, boolean expanded) {
291        super.onRowViewExpanded(holder, expanded);
292        ViewHolder vh = (ViewHolder) holder;
293        vh.mGridView.setClipToPadding(!expanded);
294        vh.mGridView.invalidate();
295        updateFooterViewSwitcher(vh);
296    }
297
298    @Override
299    public void onBindViewHolder(Presenter.ViewHolder holder, Object item) {
300        super.onBindViewHolder(holder, item);
301        ViewHolder vh = (ViewHolder)holder;
302        ListRow rowItem = (ListRow) item;
303        vh.mItemBridgeAdapter.clear();
304        vh.mItemBridgeAdapter.setAdapter(rowItem.getAdapter());
305        vh.mGridView.setAdapter(vh.mItemBridgeAdapter);
306    }
307
308    @Override
309    public void onUnbindViewHolder(Presenter.ViewHolder holder) {
310        ViewHolder vh = (ViewHolder)holder;
311        vh.mGridView.setAdapter(null);
312        super.onUnbindViewHolder(holder);
313    }
314
315    /**
316     * ListRowPresenter overrides the default select effect of {@link RowPresenter}
317     * and return false.
318     */
319    @Override
320    public final boolean isUsingDefaultSelectEffect() {
321        return false;
322    }
323
324    /**
325     * Returns true so that default select effect is applied to each individual
326     * child of {@link HorizontalGridView}.  Subclass may return false to disable
327     * the default implementation.
328     * @see #onSelectLevelChanged(RowPresenter.ViewHolder)
329     */
330    public boolean isUsingDefaultListSelectEffect() {
331        return true;
332    }
333
334    /**
335     * Returns true if SDK >= 18, where default shadow
336     * is applied to each individual child of {@link HorizontalGridView}.
337     * Subclass may return false to disable.
338     */
339    public boolean isUsingDefaultShadow() {
340        return ShadowOverlayContainer.supportsShadow();
341    }
342
343    /**
344     * Enable or disable child shadow.
345     * This is not only for enable/disable default shadow implementation but also subclass must
346     * respect this flag.
347     */
348    public final void setShadowEnabled(boolean enabled) {
349        mShadowEnabled = enabled;
350    }
351
352    /**
353     * Returns true if child shadow is enabled.
354     * This is not only for enable/disable default shadow implementation but also subclass must
355     * respect this flag.
356     */
357    public final boolean getShadowEnabled() {
358        return mShadowEnabled;
359    }
360
361    final boolean needsDefaultShadow() {
362        return isUsingDefaultShadow() && getShadowEnabled();
363    }
364
365    @Override
366    public boolean canDrawOutOfBounds() {
367        return needsDefaultShadow();
368    }
369
370    /**
371     * Applies select level to header and draw a default color dim over each child
372     * of {@link HorizontalGridView}.
373     * <p>
374     * Subclass may override this method.  A subclass
375     * needs to call super.onSelectLevelChanged() for applying header select level
376     * and optionally applying a default select level to each child view of
377     * {@link HorizontalGridView} if {@link #isUsingDefaultListSelectEffect()}
378     * is true.  Subclass may override {@link #isUsingDefaultListSelectEffect()} to return
379     * false and deal with the individual item select level by itself.
380     * </p>
381     */
382    @Override
383    protected void onSelectLevelChanged(RowPresenter.ViewHolder holder) {
384        super.onSelectLevelChanged(holder);
385        if (needsDefaultSelectEffect()) {
386            ViewHolder vh = (ViewHolder) holder;
387            vh.mColorDimmer.setActiveLevel(holder.mSelectLevel);
388            int dimmedColor = vh.mColorDimmer.getPaint().getColor();
389            for (int i = 0, count = vh.mGridView.getChildCount(); i < count; i++) {
390                ShadowOverlayContainer wrapper = (ShadowOverlayContainer) vh.mGridView.getChildAt(i);
391                wrapper.setOverlayColor(dimmedColor);
392            }
393        }
394    }
395
396}
397