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.support.v17.leanback.app.HeadersFragment;
17import android.support.v17.leanback.graphics.ColorOverlayDimmer;
18import android.view.View;
19import 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 */
73public 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
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         */
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         */
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         */
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         */
136        public final boolean isSelected() {
137            return mSelected;
138        }
139
140        /**
141         * Returns the current selection level of the Row.
142         */
143        public final float getSelectLevel() {
144            return mSelectLevel;
145        }
146
147        /**
148         * Returns the view holder for the Row header for this Row.
149         */
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
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     */
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     */
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     */
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     */
222    public final RowHeaderPresenter getHeaderPresenter() {
223        return mHeaderPresenter;
224    }
225
226    /**
227     * Get the {@link RowPresenter.ViewHolder} from the given Presenter
228     * ViewHolder.
229     */
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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
360    public boolean isUsingDefaultSelectEffect() {
361        return true;
362    }
363
364    final boolean needsDefaultSelectEffect() {
365        return isUsingDefaultSelectEffect() && getSelectEffectEnabled();
366    }
367
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     */
375    public boolean canDrawOutOfBounds() {
376        return false;
377    }
378
379    @Override
380    public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
381        onBindRowViewHolder(getRowViewHolder(viewHolder), item);
382    }
383
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
392    public final void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
393        onUnbindRowViewHolder(getRowViewHolder(viewHolder));
394    }
395
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
404    public final void onViewAttachedToWindow(Presenter.ViewHolder holder) {
405        onRowViewAttachedToWindow(getRowViewHolder(holder));
406    }
407
408    protected void onRowViewAttachedToWindow(ViewHolder vh) {
409        if (vh.mHeaderViewHolder != null) {
410            mHeaderPresenter.onViewAttachedToWindow(vh.mHeaderViewHolder);
411        }
412    }
413
414    @Override
415    public final void onViewDetachedFromWindow(Presenter.ViewHolder holder) {
416        onRowViewDetachedFromWindow(getRowViewHolder(holder));
417    }
418
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     */
431    public final void setOnItemSelectedListener(OnItemSelectedListener listener) {
432        mOnItemSelectedListener = listener;
433    }
434
435    /**
436     * Get the listener for item or row selection.
437     */
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     */
451    public final void setOnItemClickedListener(OnItemClickedListener listener) {
452        mOnItemClickedListener = listener;
453    }
454
455    /**
456     * Get the listener for item click events.
457     */
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     */
467    public final void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
468        mOnItemViewSelectedListener = listener;
469    }
470
471    /**
472     * Get listener for item or row selection.
473     */
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     */
485    public final void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
486        mOnItemViewClickedListener = listener;
487    }
488
489    /**
490     * Set listener for item click event.
491     */
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     */
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     */
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