RowPresenter.java revision 2f97594742886d045ca1ce409ebc6e6e780452f6
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.view.View;
18import android.view.ViewGroup;
19
20/**
21 * A presenter that renders {@link Row}.
22 *
23 * <h3>Customize UI widgets</h3>
24 * When subclass of RowPresenter adds UI widgets,  it should subclass
25 * {@link RowPresenter.ViewHolder} and override {@link #createRowViewHolder(ViewGroup)}
26 * and {@link #initializeRowViewHolder(ViewHolder)}.  Subclass must use layout id
27 * "row_content" for the widget that will be aligned to title of {@link HeadersFragment}.
28 * RowPresenter contains an optional and replaceable {@link RowHeaderPresenter} that
29 * renders header.  User can disable default rendering or replace with a new header presenter
30 * by calling {@link #setHeaderPresenter(RowHeaderPresenter)}.
31 *
32 * <h3>UI events from fragments</h3>
33 * In addition to {@link Presenter} which defines how to render and bind data to row view,
34 * RowPresenter receives calls from upper level(typically a fragment) when:
35 * <ul>
36 * <li>
37 * Row is selected via {@link #setRowViewSelected(Presenter.ViewHolder, boolean)}.  The event
38 * is triggered immediately when there is a row selection change before the selection
39 * animation is started.
40 * Subclass of RowPresenter may override and add more works in
41 * {@link #onRowViewSelected(ViewHolder, boolean)}.
42 * </li>
43 * <li>
44 * 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 * Subclass of RowPresenter may override and add more works in
47 * {@link #onRowViewExpanded(ViewHolder, boolean)}.
48 * </li>
49 * </ul>
50 *
51 * <h3>User events:</h3>
52 * RowPresenter provides {@link OnItemSelectedListener} and {@link OnItemClickedListener}.
53 * If subclass wants to add its own {@link View.OnFocusChangeListener} or
54 * {@link View.OnClickListener}, it must do that in {@link #createRowViewHolder(ViewGroup)}
55 * to be properly chained by framework.  Adding view listeners after
56 * {@link #createRowViewHolder(ViewGroup)} will interfere framework's listeners.
57 *
58 * <h3>Selection animation</h3>
59 * <p>
60 * When user scrolls through rows,  fragment will initiate animation and call
61 * {@link #setSelectLevel(Presenter.ViewHolder, float)} with float value 0~1.  By default, fragment
62 * draws a dim overlay on top of row view for views not selected.  Subclass may override
63 * this default effect by having {@link #isUsingDefaultSelectEffect()} return false
64 * and override {@link #onSelectLevelChanged(ViewHolder)} to apply its own selection effect.
65 * </p>
66 * <p>
67 * Call {@link #setSelectEffectEnabled(boolean)} to enable/disable select effect,
68 * This is not only for enable/disable default dim implementation but also subclass must
69 * respect this flag.
70 * </p>
71 */
72public abstract class RowPresenter extends Presenter {
73
74    static class ContainerViewHolder extends Presenter.ViewHolder {
75        /**
76         * wrapped row view holder
77         */
78        final ViewHolder mRowViewHolder;
79
80        public ContainerViewHolder(RowContainerView containerView, ViewHolder rowViewHolder) {
81            super(containerView);
82            containerView.addRowView(rowViewHolder.view);
83            if (rowViewHolder.mHeaderViewHolder != null) {
84                containerView.addHeaderView(rowViewHolder.mHeaderViewHolder.view);
85            }
86            mRowViewHolder = rowViewHolder;
87            mRowViewHolder.mContainerViewHolder = this;
88        }
89    }
90
91    public static class ViewHolder extends Presenter.ViewHolder {
92        ContainerViewHolder mContainerViewHolder;
93        RowHeaderPresenter.ViewHolder mHeaderViewHolder;
94        Row mRow;
95        boolean mSelected;
96        boolean mExpanded;
97        boolean mInitialzed;
98        float mSelectLevel = 0f; // initially unselected
99        public ViewHolder(View view) {
100            super(view);
101        }
102        public final Row getRow() {
103            return mRow;
104        }
105        public final boolean isExpanded() {
106            return mExpanded;
107        }
108        public final boolean isSelected() {
109            return mSelected;
110        }
111        public final float getSelectLevel() {
112            return mSelectLevel;
113        }
114        public final RowHeaderPresenter.ViewHolder getHeaderViewHolder() {
115            return mHeaderViewHolder;
116        }
117    }
118
119    private RowHeaderPresenter mHeaderPresenter = new RowHeaderPresenter();
120    private OnItemSelectedListener mOnItemSelectedListener;
121    private OnItemClickedListener mOnItemClickedListener;
122
123    boolean mSelectEffectEnabled = true;
124
125    @Override
126    public final Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) {
127        ViewHolder vh = createRowViewHolder(parent);
128        vh.mInitialzed = false;
129        Presenter.ViewHolder result;
130        if (needsRowContainerView()) {
131            RowContainerView containerView = new RowContainerView(parent.getContext());
132            if (mHeaderPresenter != null) {
133                vh.mHeaderViewHolder = (RowHeaderPresenter.ViewHolder)
134                        mHeaderPresenter.onCreateViewHolder((ViewGroup) vh.view);
135            }
136            result = new ContainerViewHolder(containerView, vh);
137        } else {
138            result = vh;
139        }
140        initializeRowViewHolder(vh);
141        if (!vh.mInitialzed) {
142            throw new RuntimeException("super.initializeRowViewHolder() must be called");
143        }
144        return result;
145    }
146
147    /**
148     * Called to create a ViewHolder object for row,  subclass of {@link RowPresenter}
149     * should override and return a different concrete ViewHolder object.
150     */
151    protected abstract ViewHolder createRowViewHolder(ViewGroup parent);
152
153    /**
154     * Called after a {@link RowPresenter.ViewHolder} is created,
155     * subclass of {@link RowPresenter} may override this method and start with calling
156     * super.initializeRowViewHolder(ViewHolder).
157     */
158    protected void initializeRowViewHolder(ViewHolder vh) {
159        vh.mInitialzed = true;
160    }
161
162    /**
163     * Change the presenter used for rendering header. Can be null to disable header rendering.
164     * The method must be called before creating any row view.
165     */
166    public final void setHeaderPresenter(RowHeaderPresenter headerPresenter) {
167        mHeaderPresenter = headerPresenter;
168    }
169
170    /**
171     * Get optional presenter used for rendering header.  May return null.
172     */
173    public final RowHeaderPresenter getHeaderPresenter() {
174        return mHeaderPresenter;
175    }
176
177    /**
178     * Get wrapped {@link RowPresenter.ViewHolder}
179     */
180    public final ViewHolder getRowViewHolder(Presenter.ViewHolder holder) {
181        if (holder instanceof ContainerViewHolder) {
182            return ((ContainerViewHolder) holder).mRowViewHolder;
183        } else {
184            return (ViewHolder) holder;
185        }
186    }
187
188    /**
189     * Change expanded state of row view.
190     */
191    public final void setRowViewExpanded(Presenter.ViewHolder holder, boolean expanded) {
192        ViewHolder rowViewHolder = getRowViewHolder(holder);
193        rowViewHolder.mExpanded = expanded;
194        onRowViewExpanded(rowViewHolder, expanded);
195    }
196
197    /**
198     * Change select state of row view.
199     */
200    public final void setRowViewSelected(Presenter.ViewHolder holder, boolean selected) {
201        ViewHolder rowViewHolder = getRowViewHolder(holder);
202        rowViewHolder.mSelected = selected;
203        onRowViewSelected(rowViewHolder, selected);
204    }
205
206    /**
207     * Subclass may override and respond to expanded state change of row in fragment.
208     * Default implementation hide/show header view depending on expanded state.
209     * Subclass may make visual changes to row view but not allowed to create
210     * animation on the row view.
211     */
212    protected void onRowViewExpanded(ViewHolder vh, boolean expanded) {
213        updateHeaderViewVisibility(vh);
214        vh.view.setActivated(expanded);
215    }
216
217    /**
218     * Subclass may override and respond to event Row is selected.
219     * Subclass may make visual changes to row view but not allowed to create
220     * animation on the row view.
221     */
222    protected void onRowViewSelected(ViewHolder vh, boolean selected) {
223        if (selected && mOnItemSelectedListener != null) {
224            mOnItemSelectedListener.onItemSelected(null, vh.getRow());
225        }
226        updateHeaderViewVisibility(vh);
227    }
228
229    private void updateHeaderViewVisibility(ViewHolder vh) {
230        // Use remove/add for selected row for header reparenting transition.
231        // Use hide/show for unselected row for header fade out/in transition.
232        if (mHeaderPresenter != null && vh.mHeaderViewHolder != null) {
233            RowContainerView containerView = ((RowContainerView) vh.mContainerViewHolder.view);
234            View headerView = vh.mHeaderViewHolder.view;
235            if (vh.isSelected()) {
236                containerView.showHeader(true);
237                if (vh.isExpanded()) {
238                    containerView.addHeaderView(headerView);
239                } else {
240                    containerView.removeHeaderView(headerView);
241                }
242            } else {
243                containerView.addHeaderView(headerView);
244                containerView.showHeader(vh.isExpanded());
245            }
246        }
247    }
248
249    /**
250     * Set current select level from 0(unselected) to 1(selected).
251     * Subclass should override {@link #onSelectLevelChanged(ViewHolder)}.
252     */
253    public final void setSelectLevel(Presenter.ViewHolder vh, float level) {
254        ViewHolder rowViewHolder = getRowViewHolder(vh);
255        rowViewHolder.mSelectLevel = level;
256        onSelectLevelChanged(rowViewHolder);
257    }
258
259    /**
260     * Get current select level from 0(unselected) to 1(selected).
261     */
262    public final float getSelectLevel(Presenter.ViewHolder vh) {
263        return getRowViewHolder(vh).mSelectLevel;
264    }
265
266    /**
267     * Callback when select level is changed.  Default implementation applies select level
268     * to {@link RowHeaderPresenter#setSelectLevel(RowHeaderPresenter.ViewHolder, float)}
269     * when {@link #getSelectEffectEnabled()} is true.
270     * Subclass may override this function and implements its own select effect.  When it
271     * overrides,  it should also override {@link #isUsingDefaultSelectEffect()} to disable
272     * the default dimming effect applied by framework.
273     */
274    protected void onSelectLevelChanged(ViewHolder vh) {
275        if (getSelectEffectEnabled() && vh.mHeaderViewHolder != null) {
276            mHeaderPresenter.setSelectLevel(vh.mHeaderViewHolder, vh.mSelectLevel);
277        }
278    }
279
280    /**
281     * Enables or disables the row selection effect.
282     * This is not only for enable/disable default dim implementation but also subclass must
283     * respect this flag.
284     */
285    public final void setSelectEffectEnabled(boolean applyDimOnSelect) {
286        mSelectEffectEnabled = applyDimOnSelect;
287    }
288
289    /**
290     * Returns true if row selection effect is enabled.
291     * This is not only for enable/disable default dim implementation but also subclass must
292     * respect this flag.
293     */
294    public final boolean getSelectEffectEnabled() {
295        return mSelectEffectEnabled;
296    }
297
298    /**
299     * Return if using default dimming effect provided by framework (fragment).  Subclass
300     * may(most likely) return false and override {@link #onSelectLevelChanged(ViewHolder)}.
301     */
302    public boolean isUsingDefaultSelectEffect() {
303        return true;
304    }
305
306    final boolean needsDefaultSelectEffect() {
307        return isUsingDefaultSelectEffect() && getSelectEffectEnabled();
308    }
309
310    final boolean needsRowContainerView() {
311        return mHeaderPresenter != null;
312    }
313
314    /**
315     * Return true if the Row view can draw outside bounds.
316     */
317    public boolean canDrawOutOfBounds() {
318        return false;
319    }
320
321    @Override
322    public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
323        onBindRowViewHolder(getRowViewHolder(viewHolder), item);
324    }
325
326    protected void onBindRowViewHolder(ViewHolder vh, Object item) {
327        vh.mRow = (Row) item;
328        if (vh.mHeaderViewHolder != null) {
329            mHeaderPresenter.onBindViewHolder(vh.mHeaderViewHolder, item);
330        }
331    }
332
333    @Override
334    public final void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
335        onUnbindRowViewHolder(getRowViewHolder(viewHolder));
336    }
337
338    protected void onUnbindRowViewHolder(ViewHolder vh) {
339        if (vh.mHeaderViewHolder != null) {
340            mHeaderPresenter.onUnbindViewHolder(vh.mHeaderViewHolder);
341        }
342        vh.mRow = null;
343    }
344
345    @Override
346    public final void onViewAttachedToWindow(Presenter.ViewHolder holder) {
347        onRowViewAttachedToWindow(getRowViewHolder(holder));
348    }
349
350    protected void onRowViewAttachedToWindow(ViewHolder vh) {
351        if (vh.mHeaderViewHolder != null) {
352            mHeaderPresenter.onViewAttachedToWindow(vh.mHeaderViewHolder);
353        }
354    }
355
356    @Override
357    public final void onViewDetachedFromWindow(Presenter.ViewHolder holder) {
358        onRowViewDetachedFromWindow(getRowViewHolder(holder));
359    }
360
361    protected void onRowViewDetachedFromWindow(ViewHolder vh) {
362        if (vh.mHeaderViewHolder != null) {
363            mHeaderPresenter.onViewDetachedFromWindow(vh.mHeaderViewHolder);
364        }
365    }
366
367    /**
368     * Set listener for item or row selection.  RowPresenter fires row selection
369     * event with null item, subclass of RowPresenter e.g. {@link ListRowPresenter} can
370     * fire a selection event with selected item.
371     */
372    public final void setOnItemSelectedListener(OnItemSelectedListener listener) {
373        mOnItemSelectedListener = listener;
374    }
375
376    /**
377     * Get listener for item or row selection.
378     */
379    public final OnItemSelectedListener getOnItemSelectedListener() {
380        return mOnItemSelectedListener;
381    }
382
383    /**
384     * Set listener for item click event.  RowPresenter does nothing but subclass of
385     * RowPresenter may fire item click event if it does have a concept of item.
386     * OnItemClickedListener will override {@link View.OnClickListener} that
387     * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
388     * So in general,  developer should choose one of the listeners but not both.
389     */
390    public final void setOnItemClickedListener(OnItemClickedListener listener) {
391        mOnItemClickedListener = listener;
392    }
393
394    /**
395     * Set listener for item click event.
396     */
397    public final OnItemClickedListener getOnItemClickedListener() {
398        return mOnItemClickedListener;
399    }
400}
401