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