ListItem.java revision 15c4392796f5bb261c632412953db3fe753f9dda
1package androidx.car.widget;
2
3import android.car.drivingstate.CarUxRestrictions;
4import android.support.annotation.CallSuper;
5import android.support.annotation.Nullable;
6import android.support.annotation.StyleRes;
7import android.support.v7.widget.RecyclerView;
8import android.view.View;
9
10import java.util.ArrayList;
11import java.util.List;
12import java.util.function.Function;
13
14import androidx.car.R;
15
16/**
17 * Definition of items that can be inserted into {@link ListItemAdapter}.
18 *
19 * @param  ViewHolder that extends {@link ListItem.ViewHolder}.
20 */
21public abstract class ListItem<VH extends ListItem.ViewHolder> {
22
23    // Whether the item should calculate view layout params. This usually happens when the item is
24    // updated after bind() is called. Calling bind() resets to false.
25    private boolean mDirty;
26
27    // Tag for indicating whether to hide the divider.
28    private boolean mHideDivider;
29
30    private final List<ViewBinder<VH>> mCustomBinders = new ArrayList<>();
31    // Stores ViewBinders to revert customization. Does not guarantee to 1:1 match ViewBinders
32    // in mCustomerBinders.
33    private final List<ViewBinder<VH>> mCustomBinderCleanUps = new ArrayList<>();
34
35    @StyleRes private int mTitleTextAppearance = R.style.TextAppearance_Car_Body1;
36    @StyleRes private int mBodyTextAppearance = R.style.TextAppearance_Car_Body2;
37
38    /**
39     * Classes that extends {@code ListItem} should register its view type in
40     * {@link ListItemAdapter#registerListItemViewType(int, int, Function)}.
41     *
42     * @return type of this ListItem.
43     */
44    abstract int getViewType();
45
46    /**
47     * Called when ListItem is bound to its ViewHolder.
48     */
49    final void bind(VH viewHolder) {
50        // Attempt to clean up custom view binder from previous item (if any).
51        // Then save the clean up binders for next item.
52        viewHolder.cleanUp();
53        for (ViewBinder cleanUp : mCustomBinderCleanUps) {
54            viewHolder.addCleanUp(cleanUp);
55        }
56
57        if (isDirty()) {
58            resolveDirtyState();
59            markClean();
60        }
61        onBind(viewHolder);
62
63        // Custom view binders are applied after view layout.
64        for (ViewBinder<VH> binder: mCustomBinders) {
65            binder.bind(viewHolder);
66        }
67    }
68
69    /** Sets the title text appearance from the specified style resource. */
70    @CallSuper
71    void setTitleTextAppearance(@StyleRes int titleTextAppearance) {
72        mTitleTextAppearance = titleTextAppearance;
73    }
74
75    /** Sets the body text appearance from the specified style resource. */
76    @CallSuper
77    void setBodyTextAppearance(@StyleRes int bodyTextAppearance) {
78        mBodyTextAppearance = bodyTextAppearance;
79    }
80
81    /** Returns the text appearance that should be used for title text. */
82    @StyleRes
83    final int getTitleTextAppearance() {
84        return mTitleTextAppearance;
85    }
86
87    /** Returns the text appearance that should be used for body text. */
88    @StyleRes
89    final int getBodyTextAppearance() {
90        return mBodyTextAppearance;
91    }
92
93
94    /**
95     * Marks this item as dirty so {@link #resolveDirtyState()} is required in next bind() call.
96     *
97     * <p>This method should be called in each setter.
98     */
99    protected void markDirty() {
100        mDirty = true;
101    }
102
103    /**
104     * Marks this item as not dirty. No need to call {@link #resolveDirtyState()} in next bind().
105     */
106    protected void markClean() {
107        mDirty = false;
108    }
109
110    /**
111     * @return {@code true} if next bind() should call {@link #resolveDirtyState()}.
112     */
113    protected boolean isDirty() {
114        return mDirty;
115    }
116
117    /**
118     * Whether hide the item divider coming after this {@code ListItem}.
119     *
120     * <p>Note: For this to work, one must invoke
121     * {@code PagedListView.setDividerVisibilityManager(adapter} for {@link ListItemAdapter} and
122     * have dividers enabled on {@link PagedListView}.
123     */
124    public void setHideDivider(boolean hideDivider) {
125        mHideDivider = hideDivider;
126        markDirty();
127    }
128
129    /**
130     * @return {@code true} if the divider that comes after this ListItem should be hidden.
131     * Defaults to false.
132     */
133    public boolean shouldHideDivider() {
134        return mHideDivider;
135    };
136
137
138    /**
139     * Does the work that moves the ListItem from dirty state to clean state, i.e. the work required
140     * the first time this ListItem {@code bind}s to {@link ListItem.ViewHolder}.
141     * This method will transition ListItem to clean state. ListItem in clean state should move to
142     * dirty state when it is modified by calling {@link #markDirty()}.
143     */
144    protected abstract void resolveDirtyState();
145
146    /**
147     * Binds this ListItem to {@code viewHolder} by applying data in ListItem to sub-views.
148     * Assume {@link ViewHolder#cleanUp()} has already been invoked.
149     */
150    protected abstract void onBind(VH viewHolder);
151
152    /**
153     * Same as {@link #addViewBinder(ViewBinder, ViewBinder)} when {@code cleanUp} ViewBinder
154     * is null.
155     *
156     * @param binder to interact with subviews in {@code ViewHolder}.
157     *
158     * @see #addViewBinder(ViewBinder, ViewBinder)
159     */
160    public final void addViewBinder(ViewBinder<VH> binder) {
161        addViewBinder(binder, null);
162    }
163
164    /**
165     * Adds {@link ViewBinder} to interact with sub-views in {@link ViewHolder}. These ViewBinders
166     * will always be applied after {@link #onBind(ViewHolder)}.
167     *
168     * <p>To interact with a foobar sub-view in {@code ViewHolder}, make sure to first set its
169     * visibility, or call setFoobar() setter method.
170     *
171     * <p>Example:
172     * <pre>
173     * {@code
174     * TextListItem item = new TextListItem(context);
175     * item.setTitle("title");
176     * item.addViewBinder((viewHolder) -> {
177     *     viewHolder.getTitle().doFoobar();
178     * }, (viewHolder) -> {
179     *     viewHolder.getTitle().revertFoobar();
180     * });
181     * }
182     * </pre>
183     *
184     * @params binder to interact with subviews in {@code ViewHolder}.
185     * @params cleanUp view binder to revert the effect of {@code binder}. cleanUp binders will be
186     *                 stored in {@link ListItem.ViewHolder} and should be invoked via
187     *                 {@link ViewHolder#cleanUp()} before {@code ViewHolder} is recycled.
188     *                 This is to avoid changed made to ViewHolder lingers around when ViewHolder is
189     *                 recycled. Pass in null to skip.
190     */
191    public final void addViewBinder(ViewBinder<VH> binder, @Nullable ViewBinder<VH> cleanUp) {
192        mCustomBinders.add(binder);
193        if (cleanUp != null) {
194            mCustomBinderCleanUps.add(cleanUp);
195        }
196        markDirty();
197    }
198
199    /**
200     * Removes the first occurrence of the specified item.
201     *
202     * @param binder to be removed.
203     * @return {@code true} if {@code binder} exists. {@code false} otherwise.
204     */
205    public boolean removeViewBinder(ViewBinder<VH> binder) {
206        return mCustomBinders.remove(binder);
207    }
208
209    /**
210     * Functional interface to provide a way to interact with views in {@code ViewHolder}.
211     * {@code ListItem} calls all added ViewBinders when it {@code bind}s to {@code ViewHolder}.
212     *
213     * @param  class that extends {@link RecyclerView.ViewHolder}.
214     */
215    public interface ViewBinder<VH> {
216        /**
217         * Provides a way to interact with views in view holder.
218         */
219        void bind(VH viewHolder);
220    }
221
222    /**
223     * ViewHolder that supports {@link ViewBinder}.
224     */
225    public abstract static class ViewHolder extends RecyclerView.ViewHolder {
226        private final List<ViewBinder> mCleanUps = new ArrayList<>();
227
228        public ViewHolder(View itemView) {
229            super(itemView);
230        }
231
232        /**
233         * Removes customization from previous ListItem. Intended to be used when this ViewHolder is
234         * bound to a ListItem.
235         */
236        public final void cleanUp() {
237            for (ViewBinder binder : mCleanUps) {
238                binder.bind(this);
239            }
240        }
241
242        /**
243         * Stores clean up ViewBinders that will be called in {@code cleanUp()}.
244         */
245        public final void addCleanUp(@Nullable ViewBinder<ViewHolder> cleanUp) {
246            if (cleanUp != null) {
247                mCleanUps.add(cleanUp);
248            }
249        }
250
251        /**
252         * Update children views to comply with UX restriction changes.
253         *
254         * @param restrictions current car UX restrictions.
255         */
256        abstract void complyWithUxRestrictions(CarUxRestrictions restrictions);
257    }
258}
259