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  */
14 package androidx.leanback.widget;
15 
16 import android.content.Context;
17 import android.content.res.TypedArray;
18 import android.util.Log;
19 import android.view.KeyEvent;
20 import android.view.View;
21 import android.view.ViewGroup;
22 
23 import androidx.leanback.R;
24 import androidx.leanback.system.Settings;
25 import androidx.leanback.transition.TransitionHelper;
26 import androidx.recyclerview.widget.RecyclerView;
27 
28 import java.util.HashMap;
29 
30 /**
31  * ListRowPresenter renders {@link ListRow} using a
32  * {@link HorizontalGridView} hosted in a {@link ListRowView}.
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>Row selection animation</h3>
40  * ListRowPresenter disables {@link RowPresenter}'s default full row dimming effect and draws
41  * a dim overlay on each child individually.  A subclass may disable the overlay on each child
42  * by overriding {@link #isUsingDefaultListSelectEffect()} to return false and write its own child
43  * dim effect in {@link #applySelectLevelToChild(ViewHolder, View)}.
44  *
45  * <h3>Shadow</h3>
46  * ListRowPresenter applies a default shadow to each child view.  Call
47  * {@link #setShadowEnabled(boolean)} to disable shadows.  A subclass may override and return
48  * false in {@link #isUsingDefaultShadow()} and replace with its own shadow implementation.
49  */
50 public class ListRowPresenter extends RowPresenter {
51 
52     private static final String TAG = "ListRowPresenter";
53     private static final boolean DEBUG = false;
54 
55     private static final int DEFAULT_RECYCLED_POOL_SIZE = 24;
56 
57     /**
58      * ViewHolder for the ListRowPresenter.
59      */
60     public static class ViewHolder extends RowPresenter.ViewHolder {
61         final ListRowPresenter mListRowPresenter;
62         final HorizontalGridView mGridView;
63         ItemBridgeAdapter mItemBridgeAdapter;
64         final HorizontalHoverCardSwitcher mHoverCardViewSwitcher = new HorizontalHoverCardSwitcher();
65         final int mPaddingTop;
66         final int mPaddingBottom;
67         final int mPaddingLeft;
68         final int mPaddingRight;
69 
ViewHolder(View rootView, HorizontalGridView gridView, ListRowPresenter p)70         public ViewHolder(View rootView, HorizontalGridView gridView, ListRowPresenter p) {
71             super(rootView);
72             mGridView = gridView;
73             mListRowPresenter = p;
74             mPaddingTop = mGridView.getPaddingTop();
75             mPaddingBottom = mGridView.getPaddingBottom();
76             mPaddingLeft = mGridView.getPaddingLeft();
77             mPaddingRight = mGridView.getPaddingRight();
78         }
79 
80         /**
81          * Gets ListRowPresenter that creates this ViewHolder.
82          * @return ListRowPresenter that creates this ViewHolder.
83          */
getListRowPresenter()84         public final ListRowPresenter getListRowPresenter() {
85             return mListRowPresenter;
86         }
87 
88         /**
89          * Gets HorizontalGridView that shows a list of items.
90          * @return HorizontalGridView that shows a list of items.
91          */
getGridView()92         public final HorizontalGridView getGridView() {
93             return mGridView;
94         }
95 
96         /**
97          * Gets ItemBridgeAdapter that creates the list of items.
98          * @return ItemBridgeAdapter that creates the list of items.
99          */
getBridgeAdapter()100         public final ItemBridgeAdapter getBridgeAdapter() {
101             return mItemBridgeAdapter;
102         }
103 
104         /**
105          * Gets selected item position in adapter.
106          * @return Selected item position in adapter.
107          */
getSelectedPosition()108         public int getSelectedPosition() {
109             return mGridView.getSelectedPosition();
110         }
111 
112         /**
113          * Gets ViewHolder at a position in adapter.  Returns null if the item does not exist
114          * or the item is not bound to a view.
115          * @param position Position of the item in adapter.
116          * @return ViewHolder bounds to the item.
117          */
getItemViewHolder(int position)118         public Presenter.ViewHolder getItemViewHolder(int position) {
119             ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) mGridView
120                     .findViewHolderForAdapterPosition(position);
121             if (ibvh == null) {
122                 return null;
123             }
124             return ibvh.getViewHolder();
125         }
126 
127         @Override
getSelectedItemViewHolder()128         public Presenter.ViewHolder getSelectedItemViewHolder() {
129             return getItemViewHolder(getSelectedPosition());
130         }
131 
132         @Override
getSelectedItem()133         public Object getSelectedItem() {
134             ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) mGridView
135                     .findViewHolderForAdapterPosition(getSelectedPosition());
136             if (ibvh == null) {
137                 return null;
138             }
139             return ibvh.getItem();
140         }
141     }
142 
143     /**
144      * A task on the ListRowPresenter.ViewHolder that can select an item by position in the
145      * HorizontalGridView and perform an optional item task on it.
146      */
147     public static class SelectItemViewHolderTask extends Presenter.ViewHolderTask {
148 
149         private int mItemPosition;
150         private boolean mSmoothScroll = true;
151         Presenter.ViewHolderTask mItemTask;
152 
SelectItemViewHolderTask(int itemPosition)153         public SelectItemViewHolderTask(int itemPosition) {
154             setItemPosition(itemPosition);
155         }
156 
157         /**
158          * Sets the adapter position of item to select.
159          * @param itemPosition Position of the item in adapter.
160          */
setItemPosition(int itemPosition)161         public void setItemPosition(int itemPosition) {
162             mItemPosition = itemPosition;
163         }
164 
165         /**
166          * Returns the adapter position of item to select.
167          * @return The adapter position of item to select.
168          */
getItemPosition()169         public int getItemPosition() {
170             return mItemPosition;
171         }
172 
173         /**
174          * Sets smooth scrolling to the item or jump to the item without scrolling.  By default it is
175          * true.
176          * @param smoothScroll True for smooth scrolling to the item, false otherwise.
177          */
setSmoothScroll(boolean smoothScroll)178         public void setSmoothScroll(boolean smoothScroll) {
179             mSmoothScroll = smoothScroll;
180         }
181 
182         /**
183          * Returns true if smooth scrolling to the item false otherwise.  By default it is true.
184          * @return True for smooth scrolling to the item, false otherwise.
185          */
isSmoothScroll()186         public boolean isSmoothScroll() {
187             return mSmoothScroll;
188         }
189 
190         /**
191          * Returns optional task to run when the item is selected, null for no task.
192          * @return Optional task to run when the item is selected, null for no task.
193          */
getItemTask()194         public Presenter.ViewHolderTask getItemTask() {
195             return mItemTask;
196         }
197 
198         /**
199          * Sets task to run when the item is selected, null for no task.
200          * @param itemTask Optional task to run when the item is selected, null for no task.
201          */
setItemTask(Presenter.ViewHolderTask itemTask)202         public void setItemTask(Presenter.ViewHolderTask itemTask) {
203             mItemTask = itemTask;
204         }
205 
206         @Override
run(Presenter.ViewHolder holder)207         public void run(Presenter.ViewHolder holder) {
208             if (holder instanceof ListRowPresenter.ViewHolder) {
209                 HorizontalGridView gridView = ((ListRowPresenter.ViewHolder) holder).getGridView();
210                 androidx.leanback.widget.ViewHolderTask task = null;
211                 if (mItemTask != null) {
212                     task = new androidx.leanback.widget.ViewHolderTask() {
213                         final Presenter.ViewHolderTask itemTask = mItemTask;
214                         @Override
215                         public void run(RecyclerView.ViewHolder rvh) {
216                             ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) rvh;
217                             itemTask.run(ibvh.getViewHolder());
218                         }
219                     };
220                 }
221                 if (isSmoothScroll()) {
222                     gridView.setSelectedPositionSmooth(mItemPosition, task);
223                 } else {
224                     gridView.setSelectedPosition(mItemPosition, task);
225                 }
226             }
227         }
228     }
229 
230     class ListRowPresenterItemBridgeAdapter extends ItemBridgeAdapter {
231         ListRowPresenter.ViewHolder mRowViewHolder;
232 
ListRowPresenterItemBridgeAdapter(ListRowPresenter.ViewHolder rowViewHolder)233         ListRowPresenterItemBridgeAdapter(ListRowPresenter.ViewHolder rowViewHolder) {
234             mRowViewHolder = rowViewHolder;
235         }
236 
237         @Override
onCreate(ItemBridgeAdapter.ViewHolder viewHolder)238         protected void onCreate(ItemBridgeAdapter.ViewHolder viewHolder) {
239             if (viewHolder.itemView instanceof ViewGroup) {
240                 TransitionHelper.setTransitionGroup((ViewGroup) viewHolder.itemView, true);
241             }
242             if (mShadowOverlayHelper != null) {
243                 mShadowOverlayHelper.onViewCreated(viewHolder.itemView);
244             }
245         }
246 
247         @Override
onBind(final ItemBridgeAdapter.ViewHolder viewHolder)248         public void onBind(final ItemBridgeAdapter.ViewHolder viewHolder) {
249             // Only when having an OnItemClickListener, we will attach the OnClickListener.
250             if (mRowViewHolder.getOnItemViewClickedListener() != null) {
251                 viewHolder.mHolder.view.setOnClickListener(new View.OnClickListener() {
252                     @Override
253                     public void onClick(View v) {
254                         ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
255                                 mRowViewHolder.mGridView.getChildViewHolder(viewHolder.itemView);
256                         if (mRowViewHolder.getOnItemViewClickedListener() != null) {
257                             mRowViewHolder.getOnItemViewClickedListener().onItemClicked(viewHolder.mHolder,
258                                     ibh.mItem, mRowViewHolder, (ListRow) mRowViewHolder.mRow);
259                         }
260                     }
261                 });
262             }
263         }
264 
265         @Override
onUnbind(ItemBridgeAdapter.ViewHolder viewHolder)266         public void onUnbind(ItemBridgeAdapter.ViewHolder viewHolder) {
267             if (mRowViewHolder.getOnItemViewClickedListener() != null) {
268                 viewHolder.mHolder.view.setOnClickListener(null);
269             }
270         }
271 
272         @Override
onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder)273         public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
274             applySelectLevelToChild(mRowViewHolder, viewHolder.itemView);
275             mRowViewHolder.syncActivatedStatus(viewHolder.itemView);
276         }
277 
278         @Override
onAddPresenter(Presenter presenter, int type)279         public void onAddPresenter(Presenter presenter, int type) {
280             mRowViewHolder.getGridView().getRecycledViewPool().setMaxRecycledViews(
281                     type, getRecycledPoolSize(presenter));
282         }
283     }
284 
285     private int mNumRows = 1;
286     private int mRowHeight;
287     private int mExpandedRowHeight;
288     private PresenterSelector mHoverCardPresenterSelector;
289     private int mFocusZoomFactor;
290     private boolean mUseFocusDimmer;
291     private boolean mShadowEnabled = true;
292     private int mBrowseRowsFadingEdgeLength = -1;
293     private boolean mRoundedCornersEnabled = true;
294     private boolean mKeepChildForeground = true;
295     private HashMap<Presenter, Integer> mRecycledPoolSize = new HashMap<Presenter, Integer>();
296     ShadowOverlayHelper mShadowOverlayHelper;
297     private ItemBridgeAdapter.Wrapper mShadowOverlayWrapper;
298 
299     private static int sSelectedRowTopPadding;
300     private static int sExpandedSelectedRowTopPadding;
301     private static int sExpandedRowNoHovercardBottomPadding;
302 
303     /**
304      * Constructs a ListRowPresenter with defaults.
305      * Uses {@link FocusHighlight#ZOOM_FACTOR_MEDIUM} for focus zooming and
306      * disabled dimming on focus.
307      */
ListRowPresenter()308     public ListRowPresenter() {
309         this(FocusHighlight.ZOOM_FACTOR_MEDIUM);
310     }
311 
312     /**
313      * Constructs a ListRowPresenter with the given parameters.
314      *
315      * @param focusZoomFactor Controls the zoom factor used when an item view is focused. One of
316      *         {@link FocusHighlight#ZOOM_FACTOR_NONE},
317      *         {@link FocusHighlight#ZOOM_FACTOR_SMALL},
318      *         {@link FocusHighlight#ZOOM_FACTOR_XSMALL},
319      *         {@link FocusHighlight#ZOOM_FACTOR_MEDIUM},
320      *         {@link FocusHighlight#ZOOM_FACTOR_LARGE}
321      * Dimming on focus defaults to disabled.
322      */
ListRowPresenter(int focusZoomFactor)323     public ListRowPresenter(int focusZoomFactor) {
324         this(focusZoomFactor, false);
325     }
326 
327     /**
328      * Constructs a ListRowPresenter with the given parameters.
329      *
330      * @param focusZoomFactor Controls the zoom factor used when an item view is focused. One of
331      *         {@link FocusHighlight#ZOOM_FACTOR_NONE},
332      *         {@link FocusHighlight#ZOOM_FACTOR_SMALL},
333      *         {@link FocusHighlight#ZOOM_FACTOR_XSMALL},
334      *         {@link FocusHighlight#ZOOM_FACTOR_MEDIUM},
335      *         {@link FocusHighlight#ZOOM_FACTOR_LARGE}
336      * @param useFocusDimmer determines if the FocusHighlighter will use the dimmer
337      */
ListRowPresenter(int focusZoomFactor, boolean useFocusDimmer)338     public ListRowPresenter(int focusZoomFactor, boolean useFocusDimmer) {
339         if (!FocusHighlightHelper.isValidZoomIndex(focusZoomFactor)) {
340             throw new IllegalArgumentException("Unhandled zoom factor");
341         }
342         mFocusZoomFactor = focusZoomFactor;
343         mUseFocusDimmer = useFocusDimmer;
344     }
345 
346     /**
347      * Sets the row height for rows created by this Presenter. Rows
348      * created before calling this method will not be updated.
349      *
350      * @param rowHeight Row height in pixels, or WRAP_CONTENT, or 0
351      * to use the default height.
352      */
setRowHeight(int rowHeight)353     public void setRowHeight(int rowHeight) {
354         mRowHeight = rowHeight;
355     }
356 
357     /**
358      * Returns the row height for list rows created by this Presenter.
359      */
getRowHeight()360     public int getRowHeight() {
361         return mRowHeight;
362     }
363 
364     /**
365      * Sets the expanded row height for rows created by this Presenter.
366      * If not set, expanded rows have the same height as unexpanded
367      * rows.
368      *
369      * @param rowHeight The row height in to use when the row is expanded,
370      *        in pixels, or WRAP_CONTENT, or 0 to use the default.
371      */
setExpandedRowHeight(int rowHeight)372     public void setExpandedRowHeight(int rowHeight) {
373         mExpandedRowHeight = rowHeight;
374     }
375 
376     /**
377      * Returns the expanded row height for rows created by this Presenter.
378      */
getExpandedRowHeight()379     public int getExpandedRowHeight() {
380         return mExpandedRowHeight != 0 ? mExpandedRowHeight : mRowHeight;
381     }
382 
383     /**
384      * Returns the zoom factor used for focus highlighting.
385      */
getFocusZoomFactor()386     public final int getFocusZoomFactor() {
387         return mFocusZoomFactor;
388     }
389 
390     /**
391      * Returns the zoom factor used for focus highlighting.
392      * @deprecated use {@link #getFocusZoomFactor} instead.
393      */
394     @Deprecated
getZoomFactor()395     public final int getZoomFactor() {
396         return mFocusZoomFactor;
397     }
398 
399     /**
400      * Returns true if the focus dimmer is used for focus highlighting; false otherwise.
401      */
isFocusDimmerUsed()402     public final boolean isFocusDimmerUsed() {
403         return mUseFocusDimmer;
404     }
405 
406     /**
407      * Sets the numbers of rows for rendering the list of items. By default, it is
408      * set to 1.
409      */
setNumRows(int numRows)410     public void setNumRows(int numRows) {
411         this.mNumRows = numRows;
412     }
413 
414     @Override
initializeRowViewHolder(RowPresenter.ViewHolder holder)415     protected void initializeRowViewHolder(RowPresenter.ViewHolder holder) {
416         super.initializeRowViewHolder(holder);
417         final ViewHolder rowViewHolder = (ViewHolder) holder;
418         Context context = holder.view.getContext();
419         if (mShadowOverlayHelper == null) {
420             mShadowOverlayHelper = new ShadowOverlayHelper.Builder()
421                     .needsOverlay(needsDefaultListSelectEffect())
422                     .needsShadow(needsDefaultShadow())
423                     .needsRoundedCorner(isUsingOutlineClipping(context)
424                             && areChildRoundedCornersEnabled())
425                     .preferZOrder(isUsingZOrder(context))
426                     .keepForegroundDrawable(mKeepChildForeground)
427                     .options(createShadowOverlayOptions())
428                     .build(context);
429             if (mShadowOverlayHelper.needsWrapper()) {
430                 mShadowOverlayWrapper = new ItemBridgeAdapterShadowOverlayWrapper(
431                         mShadowOverlayHelper);
432             }
433         }
434         rowViewHolder.mItemBridgeAdapter = new ListRowPresenterItemBridgeAdapter(rowViewHolder);
435         // set wrapper if needed
436         rowViewHolder.mItemBridgeAdapter.setWrapper(mShadowOverlayWrapper);
437         mShadowOverlayHelper.prepareParentForShadow(rowViewHolder.mGridView);
438 
439         FocusHighlightHelper.setupBrowseItemFocusHighlight(rowViewHolder.mItemBridgeAdapter,
440                 mFocusZoomFactor, mUseFocusDimmer);
441         rowViewHolder.mGridView.setFocusDrawingOrderEnabled(mShadowOverlayHelper.getShadowType()
442                 != ShadowOverlayHelper.SHADOW_DYNAMIC);
443         rowViewHolder.mGridView.setOnChildSelectedListener(
444                 new OnChildSelectedListener() {
445             @Override
446             public void onChildSelected(ViewGroup parent, View view, int position, long id) {
447                 selectChildView(rowViewHolder, view, true);
448             }
449         });
450         rowViewHolder.mGridView.setOnUnhandledKeyListener(
451                 new BaseGridView.OnUnhandledKeyListener() {
452                 @Override
453                 public boolean onUnhandledKey(KeyEvent event) {
454                     return rowViewHolder.getOnKeyListener() != null
455                             && rowViewHolder.getOnKeyListener().onKey(
456                                     rowViewHolder.view, event.getKeyCode(), event);
457                 }
458             });
459         rowViewHolder.mGridView.setNumRows(mNumRows);
460     }
461 
needsDefaultListSelectEffect()462     final boolean needsDefaultListSelectEffect() {
463         return isUsingDefaultListSelectEffect() && getSelectEffectEnabled();
464     }
465 
466     /**
467      * Sets the recycled pool size for the given presenter.
468      */
setRecycledPoolSize(Presenter presenter, int size)469     public void setRecycledPoolSize(Presenter presenter, int size) {
470         mRecycledPoolSize.put(presenter, size);
471     }
472 
473     /**
474      * Returns the recycled pool size for the given presenter.
475      */
getRecycledPoolSize(Presenter presenter)476     public int getRecycledPoolSize(Presenter presenter) {
477         return mRecycledPoolSize.containsKey(presenter) ? mRecycledPoolSize.get(presenter) :
478                 DEFAULT_RECYCLED_POOL_SIZE;
479     }
480 
481     /**
482      * Sets the {@link PresenterSelector} used for showing a select object in a hover card.
483      */
setHoverCardPresenterSelector(PresenterSelector selector)484     public final void setHoverCardPresenterSelector(PresenterSelector selector) {
485         mHoverCardPresenterSelector = selector;
486     }
487 
488     /**
489      * Returns the {@link PresenterSelector} used for showing a select object in a hover card.
490      */
getHoverCardPresenterSelector()491     public final PresenterSelector getHoverCardPresenterSelector() {
492         return mHoverCardPresenterSelector;
493     }
494 
495     /*
496      * Perform operations when a child of horizontal grid view is selected.
497      */
selectChildView(ViewHolder rowViewHolder, View view, boolean fireEvent)498     void selectChildView(ViewHolder rowViewHolder, View view, boolean fireEvent) {
499         if (view != null) {
500             if (rowViewHolder.mSelected) {
501                 ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
502                         rowViewHolder.mGridView.getChildViewHolder(view);
503 
504                 if (mHoverCardPresenterSelector != null) {
505                     rowViewHolder.mHoverCardViewSwitcher.select(
506                             rowViewHolder.mGridView, view, ibh.mItem);
507                 }
508                 if (fireEvent && rowViewHolder.getOnItemViewSelectedListener() != null) {
509                     rowViewHolder.getOnItemViewSelectedListener().onItemSelected(
510                             ibh.mHolder, ibh.mItem, rowViewHolder, rowViewHolder.mRow);
511                 }
512             }
513         } else {
514             if (mHoverCardPresenterSelector != null) {
515                 rowViewHolder.mHoverCardViewSwitcher.unselect();
516             }
517             if (fireEvent && rowViewHolder.getOnItemViewSelectedListener() != null) {
518                 rowViewHolder.getOnItemViewSelectedListener().onItemSelected(
519                         null, null, rowViewHolder, rowViewHolder.mRow);
520             }
521         }
522     }
523 
initStatics(Context context)524     private static void initStatics(Context context) {
525         if (sSelectedRowTopPadding == 0) {
526             sSelectedRowTopPadding = context.getResources().getDimensionPixelSize(
527                     R.dimen.lb_browse_selected_row_top_padding);
528             sExpandedSelectedRowTopPadding = context.getResources().getDimensionPixelSize(
529                     R.dimen.lb_browse_expanded_selected_row_top_padding);
530             sExpandedRowNoHovercardBottomPadding = context.getResources().getDimensionPixelSize(
531                     R.dimen.lb_browse_expanded_row_no_hovercard_bottom_padding);
532         }
533     }
534 
getSpaceUnderBaseline(ListRowPresenter.ViewHolder vh)535     private int getSpaceUnderBaseline(ListRowPresenter.ViewHolder vh) {
536         RowHeaderPresenter.ViewHolder headerViewHolder = vh.getHeaderViewHolder();
537         if (headerViewHolder != null) {
538             if (getHeaderPresenter() != null) {
539                 return getHeaderPresenter().getSpaceUnderBaseline(headerViewHolder);
540             }
541             return headerViewHolder.view.getPaddingBottom();
542         }
543         return 0;
544     }
545 
setVerticalPadding(ListRowPresenter.ViewHolder vh)546     private void setVerticalPadding(ListRowPresenter.ViewHolder vh) {
547         int paddingTop, paddingBottom;
548         // Note: sufficient bottom padding needed for card shadows.
549         if (vh.isExpanded()) {
550             int headerSpaceUnderBaseline = getSpaceUnderBaseline(vh);
551             if (DEBUG) Log.v(TAG, "headerSpaceUnderBaseline " + headerSpaceUnderBaseline);
552             paddingTop = (vh.isSelected() ? sExpandedSelectedRowTopPadding : vh.mPaddingTop)
553                     - headerSpaceUnderBaseline;
554             paddingBottom = mHoverCardPresenterSelector == null
555                     ? sExpandedRowNoHovercardBottomPadding : vh.mPaddingBottom;
556         } else if (vh.isSelected()) {
557             paddingTop = sSelectedRowTopPadding - vh.mPaddingBottom;
558             paddingBottom = sSelectedRowTopPadding;
559         } else {
560             paddingTop = 0;
561             paddingBottom = vh.mPaddingBottom;
562         }
563         vh.getGridView().setPadding(vh.mPaddingLeft, paddingTop, vh.mPaddingRight,
564                 paddingBottom);
565     }
566 
567     @Override
createRowViewHolder(ViewGroup parent)568     protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
569         initStatics(parent.getContext());
570         ListRowView rowView = new ListRowView(parent.getContext());
571         setupFadingEffect(rowView);
572         if (mRowHeight != 0) {
573             rowView.getGridView().setRowHeight(mRowHeight);
574         }
575         return new ViewHolder(rowView, rowView.getGridView(), this);
576     }
577 
578     /**
579      * Dispatch item selected event using current selected item in the {@link HorizontalGridView}.
580      * The method should only be called from onRowViewSelected().
581      */
582     @Override
dispatchItemSelectedListener(RowPresenter.ViewHolder holder, boolean selected)583     protected void dispatchItemSelectedListener(RowPresenter.ViewHolder holder, boolean selected) {
584         ViewHolder vh = (ViewHolder)holder;
585         ItemBridgeAdapter.ViewHolder itemViewHolder = (ItemBridgeAdapter.ViewHolder)
586                 vh.mGridView.findViewHolderForPosition(vh.mGridView.getSelectedPosition());
587         if (itemViewHolder == null) {
588             super.dispatchItemSelectedListener(holder, selected);
589             return;
590         }
591 
592         if (selected) {
593             if (holder.getOnItemViewSelectedListener() != null) {
594                 holder.getOnItemViewSelectedListener().onItemSelected(
595                         itemViewHolder.getViewHolder(), itemViewHolder.mItem, vh, vh.getRow());
596             }
597         }
598     }
599 
600     @Override
onRowViewSelected(RowPresenter.ViewHolder holder, boolean selected)601     protected void onRowViewSelected(RowPresenter.ViewHolder holder, boolean selected) {
602         super.onRowViewSelected(holder, selected);
603         ViewHolder vh = (ViewHolder) holder;
604         setVerticalPadding(vh);
605         updateFooterViewSwitcher(vh);
606     }
607 
608     /*
609      * Show or hide hover card when row selection or expanded state is changed.
610      */
updateFooterViewSwitcher(ViewHolder vh)611     private void updateFooterViewSwitcher(ViewHolder vh) {
612         if (vh.mExpanded && vh.mSelected) {
613             if (mHoverCardPresenterSelector != null) {
614                 vh.mHoverCardViewSwitcher.init((ViewGroup) vh.view,
615                         mHoverCardPresenterSelector);
616             }
617             ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
618                     vh.mGridView.findViewHolderForPosition(
619                             vh.mGridView.getSelectedPosition());
620             selectChildView(vh, ibh == null ? null : ibh.itemView, false);
621         } else {
622             if (mHoverCardPresenterSelector != null) {
623                 vh.mHoverCardViewSwitcher.unselect();
624             }
625         }
626     }
627 
setupFadingEffect(ListRowView rowView)628     private void setupFadingEffect(ListRowView rowView) {
629         // content is completely faded at 1/2 padding of left, fading length is 1/2 of padding.
630         HorizontalGridView gridView = rowView.getGridView();
631         if (mBrowseRowsFadingEdgeLength < 0) {
632             TypedArray ta = gridView.getContext()
633                     .obtainStyledAttributes(R.styleable.LeanbackTheme);
634             mBrowseRowsFadingEdgeLength = (int) ta.getDimension(
635                     R.styleable.LeanbackTheme_browseRowsFadingEdgeLength, 0);
636             ta.recycle();
637         }
638         gridView.setFadingLeftEdgeLength(mBrowseRowsFadingEdgeLength);
639     }
640 
641     @Override
onRowViewExpanded(RowPresenter.ViewHolder holder, boolean expanded)642     protected void onRowViewExpanded(RowPresenter.ViewHolder holder, boolean expanded) {
643         super.onRowViewExpanded(holder, expanded);
644         ViewHolder vh = (ViewHolder) holder;
645         if (getRowHeight() != getExpandedRowHeight()) {
646             int newHeight = expanded ? getExpandedRowHeight() : getRowHeight();
647             vh.getGridView().setRowHeight(newHeight);
648         }
649         setVerticalPadding(vh);
650         updateFooterViewSwitcher(vh);
651     }
652 
653     @Override
onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item)654     protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
655         super.onBindRowViewHolder(holder, item);
656         ViewHolder vh = (ViewHolder) holder;
657         ListRow rowItem = (ListRow) item;
658         vh.mItemBridgeAdapter.setAdapter(rowItem.getAdapter());
659         vh.mGridView.setAdapter(vh.mItemBridgeAdapter);
660         vh.mGridView.setContentDescription(rowItem.getContentDescription());
661     }
662 
663     @Override
onUnbindRowViewHolder(RowPresenter.ViewHolder holder)664     protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) {
665         ViewHolder vh = (ViewHolder) holder;
666         vh.mGridView.setAdapter(null);
667         vh.mItemBridgeAdapter.clear();
668         super.onUnbindRowViewHolder(holder);
669     }
670 
671     /**
672      * ListRowPresenter overrides the default select effect of {@link RowPresenter}
673      * and return false.
674      */
675     @Override
isUsingDefaultSelectEffect()676     public final boolean isUsingDefaultSelectEffect() {
677         return false;
678     }
679 
680     /**
681      * Returns true so that default select effect is applied to each individual
682      * child of {@link HorizontalGridView}.  Subclass may return false to disable
683      * the default implementation and implement {@link #applySelectLevelToChild(ViewHolder, View)}.
684      * @see #applySelectLevelToChild(ViewHolder, View)
685      * @see #onSelectLevelChanged(RowPresenter.ViewHolder)
686      */
isUsingDefaultListSelectEffect()687     public boolean isUsingDefaultListSelectEffect() {
688         return true;
689     }
690 
691     /**
692      * Default implementation returns true if SDK version >= 21, shadow (either static or z-order
693      * based) will be applied to each individual child of {@link HorizontalGridView}.
694      * Subclass may return false to disable default implementation of shadow and provide its own.
695      */
isUsingDefaultShadow()696     public boolean isUsingDefaultShadow() {
697         return ShadowOverlayHelper.supportsShadow();
698     }
699 
700     /**
701      * Returns true if SDK >= L, where Z shadow is enabled so that Z order is enabled
702      * on each child of horizontal list.   If subclass returns false in isUsingDefaultShadow()
703      * and does not use Z-shadow on SDK >= L, it should override isUsingZOrder() return false.
704      */
isUsingZOrder(Context context)705     public boolean isUsingZOrder(Context context) {
706         return !Settings.getInstance(context).preferStaticShadows();
707     }
708 
709     /**
710      * Returns true if leanback view outline is enabled on the system or false otherwise. When
711      * false, rounded corner will not be enabled even {@link #enableChildRoundedCorners(boolean)}
712      * is called with true.
713      *
714      * @param context Context to retrieve system settings.
715      * @return True if leanback view outline is enabled on the system or false otherwise.
716      */
isUsingOutlineClipping(Context context)717     public boolean isUsingOutlineClipping(Context context) {
718         return !Settings.getInstance(context).isOutlineClippingDisabled();
719     }
720 
721     /**
722      * Enables or disables child shadow.
723      * This is not only for enable/disable default shadow implementation but also subclass must
724      * respect this flag.
725      */
setShadowEnabled(boolean enabled)726     public final void setShadowEnabled(boolean enabled) {
727         mShadowEnabled = enabled;
728     }
729 
730     /**
731      * Returns true if child shadow is enabled.
732      * This is not only for enable/disable default shadow implementation but also subclass must
733      * respect this flag.
734      */
getShadowEnabled()735     public final boolean getShadowEnabled() {
736         return mShadowEnabled;
737     }
738 
739     /**
740      * Enables or disabled rounded corners on children of this row.
741      * Supported on Android SDK >= L.
742      */
enableChildRoundedCorners(boolean enable)743     public final void enableChildRoundedCorners(boolean enable) {
744         mRoundedCornersEnabled = enable;
745     }
746 
747     /**
748      * Returns true if rounded corners are enabled for children of this row.
749      */
areChildRoundedCornersEnabled()750     public final boolean areChildRoundedCornersEnabled() {
751         return mRoundedCornersEnabled;
752     }
753 
needsDefaultShadow()754     final boolean needsDefaultShadow() {
755         return isUsingDefaultShadow() && getShadowEnabled();
756     }
757 
758     /**
759      * When ListRowPresenter applies overlay color on the child,  it may change child's foreground
760      * Drawable.  If application uses child's foreground for other purposes such as ripple effect,
761      * it needs tell ListRowPresenter to keep the child's foreground.  The default value is true.
762      *
763      * @param keep true if keep foreground of child of this row, false ListRowPresenter might change
764      *             the foreground of the child.
765      */
setKeepChildForeground(boolean keep)766     public final void setKeepChildForeground(boolean keep) {
767         mKeepChildForeground = keep;
768     }
769 
770     /**
771      * Returns true if keeps foreground of child of this row, false otherwise.  When
772      * ListRowPresenter applies overlay color on the child,  it may change child's foreground
773      * Drawable.  If application uses child's foreground for other purposes such as ripple effect,
774      * it needs tell ListRowPresenter to keep the child's foreground.  The default value is true.
775      *
776      * @return true if keeps foreground of child of this row, false otherwise.
777      */
isKeepChildForeground()778     public final boolean isKeepChildForeground() {
779         return mKeepChildForeground;
780     }
781 
782     /**
783      * Create ShadowOverlayHelper Options.  Subclass may override.
784      * e.g.
785      * <code>
786      * return new ShadowOverlayHelper.Options().roundedCornerRadius(10);
787      * </code>
788      *
789      * @return The options to be used for shadow, overlay and rounded corner.
790      */
createShadowOverlayOptions()791     protected ShadowOverlayHelper.Options createShadowOverlayOptions() {
792         return ShadowOverlayHelper.Options.DEFAULT;
793     }
794 
795     /**
796      * Applies select level to header and draws a default color dim over each child
797      * of {@link HorizontalGridView}.
798      * <p>
799      * Subclass may override this method and starts with calling super if it has views to apply
800      * select effect other than header and HorizontalGridView.
801      * To override the default color dim over each child of {@link HorizontalGridView},
802      * app should override {@link #isUsingDefaultListSelectEffect()} to
803      * return false and override {@link #applySelectLevelToChild(ViewHolder, View)}.
804      * </p>
805      * @see #isUsingDefaultListSelectEffect()
806      * @see RowPresenter.ViewHolder#getSelectLevel()
807      * @see #applySelectLevelToChild(ViewHolder, View)
808      */
809     @Override
onSelectLevelChanged(RowPresenter.ViewHolder holder)810     protected void onSelectLevelChanged(RowPresenter.ViewHolder holder) {
811         super.onSelectLevelChanged(holder);
812         ViewHolder vh = (ViewHolder) holder;
813         for (int i = 0, count = vh.mGridView.getChildCount(); i < count; i++) {
814             applySelectLevelToChild(vh, vh.mGridView.getChildAt(i));
815         }
816     }
817 
818     /**
819      * Applies select level to a child.  Default implementation draws a default color
820      * dim over each child of {@link HorizontalGridView}. This method is called on all children in
821      * {@link #onSelectLevelChanged(RowPresenter.ViewHolder)} and when a child is attached to
822      * {@link HorizontalGridView}.
823      * <p>
824      * Subclass may disable the default implementation by override
825      * {@link #isUsingDefaultListSelectEffect()} to return false and deal with the individual item
826      * select level by itself.
827      * </p>
828      * @param rowViewHolder The ViewHolder of the Row
829      * @param childView The child of {@link HorizontalGridView} to apply select level.
830      *
831      * @see #isUsingDefaultListSelectEffect()
832      * @see RowPresenter.ViewHolder#getSelectLevel()
833      * @see #onSelectLevelChanged(RowPresenter.ViewHolder)
834      */
applySelectLevelToChild(ViewHolder rowViewHolder, View childView)835     protected void applySelectLevelToChild(ViewHolder rowViewHolder, View childView) {
836         if (mShadowOverlayHelper != null && mShadowOverlayHelper.needsOverlay()) {
837             int dimmedColor = rowViewHolder.mColorDimmer.getPaint().getColor();
838             mShadowOverlayHelper.setOverlayColor(childView, dimmedColor);
839         }
840     }
841 
842     @Override
freeze(RowPresenter.ViewHolder holder, boolean freeze)843     public void freeze(RowPresenter.ViewHolder holder, boolean freeze) {
844         ViewHolder vh = (ViewHolder) holder;
845         vh.mGridView.setScrollEnabled(!freeze);
846         vh.mGridView.setAnimateChildLayout(!freeze);
847     }
848 
849     @Override
setEntranceTransitionState(RowPresenter.ViewHolder holder, boolean afterEntrance)850     public void setEntranceTransitionState(RowPresenter.ViewHolder holder,
851             boolean afterEntrance) {
852         super.setEntranceTransitionState(holder, afterEntrance);
853         ((ViewHolder) holder).mGridView.setChildrenVisibility(
854                 afterEntrance? View.VISIBLE : View.INVISIBLE);
855     }
856 }
857