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 android.support.v17.leanback.widget;
15 
16 import android.support.v17.leanback.app.HeadersFragment;
17 import android.support.v17.leanback.graphics.ColorOverlayDimmer;
18 import android.view.View;
19 import android.view.ViewGroup;
20 
21 /**
22  * An abstract {@link Presenter} that renders a {@link Row}.
23  *
24  * <h3>Customize UI widgets</h3>
25  * When a subclass of RowPresenter adds UI widgets, it should subclass
26  * {@link RowPresenter.ViewHolder} and override {@link #createRowViewHolder(ViewGroup)}
27  * and {@link #initializeRowViewHolder(ViewHolder)}. The subclass must use layout id
28  * "row_content" for the widget that will be aligned to the title of any {@link HeadersFragment}
29  * that may exist in the parent fragment. RowPresenter contains an optional and
30  * replaceable {@link RowHeaderPresenter} that renders the header. You can disable
31  * the default rendering or replace the Presenter with a new header presenter
32  * by calling {@link #setHeaderPresenter(RowHeaderPresenter)}.
33  *
34  * <h3>UI events from fragments</h3>
35  * RowPresenter receives calls from its parent (typically a Fragment) when:
36  * <ul>
37  * <li>
38  * A Row is selected via {@link #setRowViewSelected(Presenter.ViewHolder, boolean)}.  The event
39  * is triggered immediately when there is a row selection change before the selection
40  * animation is started.
41  * Subclasses of RowPresenter may override {@link #onRowViewSelected(ViewHolder, boolean)}.
42  * </li>
43  * <li>
44  * A Row is expanded to full width via {@link #setRowViewExpanded(Presenter.ViewHolder, boolean)}.
45  * The event is triggered immediately before the expand animation is started.
46  * Subclasses of RowPresenter may override {@link #onRowViewExpanded(ViewHolder, boolean)}.
47  * </li>
48  * </ul>
49  *
50  * <h3>User events</h3>
51  * RowPresenter provides {@link OnItemSelectedListener} and {@link OnItemClickedListener}.
52  * If a subclass wants to add its own {@link View.OnFocusChangeListener} or
53  * {@link View.OnClickListener}, it must do that in {@link #createRowViewHolder(ViewGroup)}
54  * to be properly chained by the library.  Adding View listeners after
55  * {@link #createRowViewHolder(ViewGroup)} is undefined and may result in
56  * incorrect behavior by the library's listeners.
57  *
58  * <h3>Selection animation</h3>
59  * <p>
60  * When a user scrolls through rows, a fragment will initiate animation and call
61  * {@link #setSelectLevel(Presenter.ViewHolder, float)} with float value between
62  * 0 and 1.  By default, the RowPresenter draws a dim overlay on top of the row
63  * view for views that are not selected. Subclasses may override this default effect
64  * by having {@link #isUsingDefaultSelectEffect()} return false and overriding
65  * {@link #onSelectLevelChanged(ViewHolder)} to apply a different selection effect.
66  * </p>
67  * <p>
68  * Call {@link #setSelectEffectEnabled(boolean)} to enable/disable the select effect,
69  * This will not only enable/disable the default dim effect but also subclasses must
70  * respect this flag as well.
71  * </p>
72  */
73 public abstract class RowPresenter extends Presenter {
74 
75     static class ContainerViewHolder extends Presenter.ViewHolder {
76         /**
77          * wrapped row view holder
78          */
79         final ViewHolder mRowViewHolder;
80 
ContainerViewHolder(RowContainerView containerView, ViewHolder rowViewHolder)81         public ContainerViewHolder(RowContainerView containerView, ViewHolder rowViewHolder) {
82             super(containerView);
83             containerView.addRowView(rowViewHolder.view);
84             if (rowViewHolder.mHeaderViewHolder != null) {
85                 containerView.addHeaderView(rowViewHolder.mHeaderViewHolder.view);
86             }
87             mRowViewHolder = rowViewHolder;
88             mRowViewHolder.mContainerViewHolder = this;
89         }
90     }
91 
92     /**
93      * A view holder for a {@link Row}.
94      */
95     public static class ViewHolder extends Presenter.ViewHolder {
96         ContainerViewHolder mContainerViewHolder;
97         RowHeaderPresenter.ViewHolder mHeaderViewHolder;
98         Row mRow;
99         boolean mSelected;
100         boolean mExpanded;
101         boolean mInitialzed;
102         float mSelectLevel = 0f; // initially unselected
103         protected final ColorOverlayDimmer mColorDimmer;
104 
105         /**
106          * Constructor for ViewHolder.
107          *
108          * @param view The View bound to the Row.
109          */
ViewHolder(View view)110         public ViewHolder(View view) {
111             super(view);
112             mColorDimmer = ColorOverlayDimmer.createDefault(view.getContext());
113         }
114 
115         /**
116          * Returns the Row bound to the View in this ViewHolder.
117          */
getRow()118         public final Row getRow() {
119             return mRow;
120         }
121 
122         /**
123          * Returns whether the Row is in its expanded state.
124          *
125          * @return true if the Row is expanded, false otherwise.
126          */
isExpanded()127         public final boolean isExpanded() {
128             return mExpanded;
129         }
130 
131         /**
132          * Returns whether the Row is selected.
133          *
134          * @return true if the Row is selected, false otherwise.
135          */
isSelected()136         public final boolean isSelected() {
137             return mSelected;
138         }
139 
140         /**
141          * Returns the current selection level of the Row.
142          */
getSelectLevel()143         public final float getSelectLevel() {
144             return mSelectLevel;
145         }
146 
147         /**
148          * Returns the view holder for the Row header for this Row.
149          */
getHeaderViewHolder()150         public final RowHeaderPresenter.ViewHolder getHeaderViewHolder() {
151             return mHeaderViewHolder;
152         }
153     }
154 
155     private RowHeaderPresenter mHeaderPresenter = new RowHeaderPresenter();
156     private OnItemSelectedListener mOnItemSelectedListener;
157     private OnItemClickedListener mOnItemClickedListener;
158     private OnItemViewSelectedListener mOnItemViewSelectedListener;
159     private OnItemViewClickedListener mOnItemViewClickedListener;
160 
161     boolean mSelectEffectEnabled = true;
162 
163     @Override
onCreateViewHolder(ViewGroup parent)164     public final Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) {
165         ViewHolder vh = createRowViewHolder(parent);
166         vh.mInitialzed = false;
167         Presenter.ViewHolder result;
168         if (needsRowContainerView()) {
169             RowContainerView containerView = new RowContainerView(parent.getContext());
170             if (mHeaderPresenter != null) {
171                 vh.mHeaderViewHolder = (RowHeaderPresenter.ViewHolder)
172                         mHeaderPresenter.onCreateViewHolder((ViewGroup) vh.view);
173             }
174             result = new ContainerViewHolder(containerView, vh);
175         } else {
176             result = vh;
177         }
178         initializeRowViewHolder(vh);
179         if (!vh.mInitialzed) {
180             throw new RuntimeException("super.initializeRowViewHolder() must be called");
181         }
182         return result;
183     }
184 
185     /**
186      * Called to create a ViewHolder object for a Row. Subclasses will override
187      * this method to return a different concrete ViewHolder object.
188      *
189      * @param parent The parent View for the Row's view holder.
190      * @return A ViewHolder for the Row's View.
191      */
createRowViewHolder(ViewGroup parent)192     protected abstract ViewHolder createRowViewHolder(ViewGroup parent);
193 
194     /**
195      * Called after a {@link RowPresenter.ViewHolder} is created for a Row.
196      * Subclasses may override this method and start by calling
197      * super.initializeRowViewHolder(ViewHolder).
198      *
199      * @param vh The ViewHolder to initialize for the Row.
200      */
initializeRowViewHolder(ViewHolder vh)201     protected void initializeRowViewHolder(ViewHolder vh) {
202         vh.mInitialzed = true;
203         // set clip children to false for slide transition
204         ((ViewGroup) vh.view).setClipChildren(false);
205         if (vh.mContainerViewHolder != null) {
206             ((ViewGroup) vh.mContainerViewHolder.view).setClipChildren(false);
207         }
208     }
209 
210     /**
211      * Set the Presenter used for rendering the header. Can be null to disable
212      * header rendering. The method must be called before creating any Row Views.
213      */
setHeaderPresenter(RowHeaderPresenter headerPresenter)214     public final void setHeaderPresenter(RowHeaderPresenter headerPresenter) {
215         mHeaderPresenter = headerPresenter;
216     }
217 
218     /**
219      * Get the Presenter used for rendering the header, or null if none has been
220      * set.
221      */
getHeaderPresenter()222     public final RowHeaderPresenter getHeaderPresenter() {
223         return mHeaderPresenter;
224     }
225 
226     /**
227      * Get the {@link RowPresenter.ViewHolder} from the given Presenter
228      * ViewHolder.
229      */
getRowViewHolder(Presenter.ViewHolder holder)230     public final ViewHolder getRowViewHolder(Presenter.ViewHolder holder) {
231         if (holder instanceof ContainerViewHolder) {
232             return ((ContainerViewHolder) holder).mRowViewHolder;
233         } else {
234             return (ViewHolder) holder;
235         }
236     }
237 
238     /**
239      * Set the expanded state of a Row view.
240      *
241      * @param holder The Row ViewHolder to set expanded state on.
242      * @param expanded True if the Row is expanded, false otherwise.
243      */
setRowViewExpanded(Presenter.ViewHolder holder, boolean expanded)244     public final void setRowViewExpanded(Presenter.ViewHolder holder, boolean expanded) {
245         ViewHolder rowViewHolder = getRowViewHolder(holder);
246         rowViewHolder.mExpanded = expanded;
247         onRowViewExpanded(rowViewHolder, expanded);
248     }
249 
250     /**
251      * Set the selected state of a Row view.
252      *
253      * @param holder The Row ViewHolder to set expanded state on.
254      * @param selected True if the Row is expanded, false otherwise.
255      */
setRowViewSelected(Presenter.ViewHolder holder, boolean selected)256     public final void setRowViewSelected(Presenter.ViewHolder holder, boolean selected) {
257         ViewHolder rowViewHolder = getRowViewHolder(holder);
258         rowViewHolder.mSelected = selected;
259         onRowViewSelected(rowViewHolder, selected);
260     }
261 
262     /**
263      * Subclass may override this to respond to expanded state changes of a Row.
264      * The default implementation will hide/show the header view. Subclasses may
265      * make visual changes to the Row View but must not create animation on the
266      * Row view.
267      */
onRowViewExpanded(ViewHolder vh, boolean expanded)268     protected void onRowViewExpanded(ViewHolder vh, boolean expanded) {
269         updateHeaderViewVisibility(vh);
270         vh.view.setActivated(expanded);
271     }
272 
273     /**
274      * Subclass may override this to respond to selected state changes of a Row.
275      * Subclass may make visual changes to Row view but must not create
276      * animation on the Row view.
277      */
onRowViewSelected(ViewHolder vh, boolean selected)278     protected void onRowViewSelected(ViewHolder vh, boolean selected) {
279         if (selected) {
280             if (mOnItemViewSelectedListener != null) {
281                 mOnItemViewSelectedListener.onItemSelected(null, null, vh, vh.getRow());
282             }
283             if (mOnItemSelectedListener != null) {
284                 mOnItemSelectedListener.onItemSelected(null, vh.getRow());
285             }
286         }
287         updateHeaderViewVisibility(vh);
288     }
289 
updateHeaderViewVisibility(ViewHolder vh)290     private void updateHeaderViewVisibility(ViewHolder vh) {
291         if (mHeaderPresenter != null && vh.mHeaderViewHolder != null) {
292             RowContainerView containerView = ((RowContainerView) vh.mContainerViewHolder.view);
293             containerView.showHeader(vh.isExpanded());
294         }
295     }
296 
297     /**
298      * Set the current select level to a value between 0 (unselected) and 1 (selected).
299      * Subclasses may override {@link #onSelectLevelChanged(ViewHolder)} to
300      * respond to changes in the selected level.
301      */
setSelectLevel(Presenter.ViewHolder vh, float level)302     public final void setSelectLevel(Presenter.ViewHolder vh, float level) {
303         ViewHolder rowViewHolder = getRowViewHolder(vh);
304         rowViewHolder.mSelectLevel = level;
305         onSelectLevelChanged(rowViewHolder);
306     }
307 
308     /**
309      * Get the current select level. The value will be between 0 (unselected)
310      * and 1 (selected).
311      */
getSelectLevel(Presenter.ViewHolder vh)312     public final float getSelectLevel(Presenter.ViewHolder vh) {
313         return getRowViewHolder(vh).mSelectLevel;
314     }
315 
316     /**
317      * Callback when select level is changed. The default implementation applies
318      * the select level to {@link RowHeaderPresenter#setSelectLevel(RowHeaderPresenter.ViewHolder, float)}
319      * when {@link #getSelectEffectEnabled()} is true. Subclasses may override
320      * this function and implement a different select effect. In this case, you
321      * should also override {@link #isUsingDefaultSelectEffect()} to disable
322      * the default dimming effect applied by the library.
323      */
onSelectLevelChanged(ViewHolder vh)324     protected void onSelectLevelChanged(ViewHolder vh) {
325         if (getSelectEffectEnabled()) {
326             vh.mColorDimmer.setActiveLevel(vh.mSelectLevel);
327             if (vh.mHeaderViewHolder != null) {
328                 mHeaderPresenter.setSelectLevel(vh.mHeaderViewHolder, vh.mSelectLevel);
329             }
330             if (isUsingDefaultSelectEffect()) {
331                 ((RowContainerView) vh.mContainerViewHolder.view).setForegroundColor(
332                         vh.mColorDimmer.getPaint().getColor());
333             }
334         }
335     }
336 
337     /**
338      * Enables or disables the row selection effect.
339      * This will not only affect the default dim effect, but subclasses must
340      * respect this flag as well.
341      */
setSelectEffectEnabled(boolean applyDimOnSelect)342     public final void setSelectEffectEnabled(boolean applyDimOnSelect) {
343         mSelectEffectEnabled = applyDimOnSelect;
344     }
345 
346     /**
347      * Returns true if the row selection effect is enabled.
348      * This value not only determines whether the default dim implementation is
349      * used, but subclasses must also respect this flag.
350      */
getSelectEffectEnabled()351     public final boolean getSelectEffectEnabled() {
352         return mSelectEffectEnabled;
353     }
354 
355     /**
356      * Return whether this RowPresenter is using the default dimming effect
357      * provided by the library.  Subclasses may(most likely) return false and
358      * override {@link #onSelectLevelChanged(ViewHolder)}.
359      */
isUsingDefaultSelectEffect()360     public boolean isUsingDefaultSelectEffect() {
361         return true;
362     }
363 
needsDefaultSelectEffect()364     final boolean needsDefaultSelectEffect() {
365         return isUsingDefaultSelectEffect() && getSelectEffectEnabled();
366     }
367 
needsRowContainerView()368     final boolean needsRowContainerView() {
369         return mHeaderPresenter != null || needsDefaultSelectEffect();
370     }
371 
372     /**
373      * Return true if the Row view can draw outside its bounds.
374      */
canDrawOutOfBounds()375     public boolean canDrawOutOfBounds() {
376         return false;
377     }
378 
379     @Override
onBindViewHolder(Presenter.ViewHolder viewHolder, Object item)380     public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
381         onBindRowViewHolder(getRowViewHolder(viewHolder), item);
382     }
383 
onBindRowViewHolder(ViewHolder vh, Object item)384     protected void onBindRowViewHolder(ViewHolder vh, Object item) {
385         vh.mRow = (Row) item;
386         if (vh.mHeaderViewHolder != null) {
387             mHeaderPresenter.onBindViewHolder(vh.mHeaderViewHolder, item);
388         }
389     }
390 
391     @Override
onUnbindViewHolder(Presenter.ViewHolder viewHolder)392     public final void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
393         onUnbindRowViewHolder(getRowViewHolder(viewHolder));
394     }
395 
onUnbindRowViewHolder(ViewHolder vh)396     protected void onUnbindRowViewHolder(ViewHolder vh) {
397         if (vh.mHeaderViewHolder != null) {
398             mHeaderPresenter.onUnbindViewHolder(vh.mHeaderViewHolder);
399         }
400         vh.mRow = null;
401     }
402 
403     @Override
onViewAttachedToWindow(Presenter.ViewHolder holder)404     public final void onViewAttachedToWindow(Presenter.ViewHolder holder) {
405         onRowViewAttachedToWindow(getRowViewHolder(holder));
406     }
407 
onRowViewAttachedToWindow(ViewHolder vh)408     protected void onRowViewAttachedToWindow(ViewHolder vh) {
409         if (vh.mHeaderViewHolder != null) {
410             mHeaderPresenter.onViewAttachedToWindow(vh.mHeaderViewHolder);
411         }
412     }
413 
414     @Override
onViewDetachedFromWindow(Presenter.ViewHolder holder)415     public final void onViewDetachedFromWindow(Presenter.ViewHolder holder) {
416         onRowViewDetachedFromWindow(getRowViewHolder(holder));
417     }
418 
onRowViewDetachedFromWindow(ViewHolder vh)419     protected void onRowViewDetachedFromWindow(ViewHolder vh) {
420         if (vh.mHeaderViewHolder != null) {
421             mHeaderPresenter.onViewDetachedFromWindow(vh.mHeaderViewHolder);
422         }
423         cancelAnimationsRecursive(vh.view);
424     }
425 
426     /**
427      * Set the listener for item or row selection. A RowPresenter fires a row
428      * selection event with a null item. Subclasses (e.g. {@link ListRowPresenter})
429      * can fire a selection event with the selected item.
430      */
setOnItemSelectedListener(OnItemSelectedListener listener)431     public final void setOnItemSelectedListener(OnItemSelectedListener listener) {
432         mOnItemSelectedListener = listener;
433     }
434 
435     /**
436      * Get the listener for item or row selection.
437      */
getOnItemSelectedListener()438     public final OnItemSelectedListener getOnItemSelectedListener() {
439         return mOnItemSelectedListener;
440     }
441 
442     /**
443      * Set the listener for item click events. A RowPresenter does not use this
444      * listener, but a subclass may fire an item click event if it has the concept
445      * of an item. The {@link OnItemClickedListener} will override any
446      * {@link View.OnClickListener} that an item's Presenter sets during
447      * {@link Presenter#onCreateViewHolder(ViewGroup)}. So in general, you
448      * should choose to use an OnItemClickedListener or a {@link
449      * View.OnClickListener}, but not both.
450      */
setOnItemClickedListener(OnItemClickedListener listener)451     public final void setOnItemClickedListener(OnItemClickedListener listener) {
452         mOnItemClickedListener = listener;
453     }
454 
455     /**
456      * Get the listener for item click events.
457      */
getOnItemClickedListener()458     public final OnItemClickedListener getOnItemClickedListener() {
459         return mOnItemClickedListener;
460     }
461 
462     /**
463      * Set listener for item or row selection.  RowPresenter fires row selection
464      * event with null item, subclass of RowPresenter e.g. {@link ListRowPresenter} can
465      * fire a selection event with selected item.
466      */
setOnItemViewSelectedListener(OnItemViewSelectedListener listener)467     public final void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
468         mOnItemViewSelectedListener = listener;
469     }
470 
471     /**
472      * Get listener for item or row selection.
473      */
getOnItemViewSelectedListener()474     public final OnItemViewSelectedListener getOnItemViewSelectedListener() {
475         return mOnItemViewSelectedListener;
476     }
477 
478     /**
479      * Set listener for item click event.  RowPresenter does nothing but subclass of
480      * RowPresenter may fire item click event if it does have a concept of item.
481      * OnItemViewClickedListener will override {@link View.OnClickListener} that
482      * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
483      * So in general,  developer should choose one of the listeners but not both.
484      */
setOnItemViewClickedListener(OnItemViewClickedListener listener)485     public final void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
486         mOnItemViewClickedListener = listener;
487     }
488 
489     /**
490      * Set listener for item click event.
491      */
getOnItemViewClickedListener()492     public final OnItemViewClickedListener getOnItemViewClickedListener() {
493         return mOnItemViewClickedListener;
494     }
495 
496     /**
497      * Freeze/Unfreeze the row, typically used when transition starts/ends.
498      * This method is called by fragment, app should not call it directly.
499      */
freeze(ViewHolder holder, boolean freeze)500     public void freeze(ViewHolder holder, boolean freeze) {
501     }
502 
503     /**
504      * Change visibility of views, entrance transition will be run against the views that
505      * change visibilities.  Subclass may override and begin with calling
506      * super.setEntranceTransitionState().  This method is called by fragment,
507      * app should not call it directly.
508      */
setEntranceTransitionState(ViewHolder holder, boolean afterTransition)509     public void setEntranceTransitionState(ViewHolder holder, boolean afterTransition) {
510         if (holder.mHeaderViewHolder != null) {
511             holder.mHeaderViewHolder.view.setVisibility(afterTransition ?
512                     View.VISIBLE : View.INVISIBLE);
513         }
514     }
515 }
516