ListItem.java revision 52a80988da3d27f591abd43faa27eae120996ec2
1package androidx.car.widget;
2
3import android.car.drivingstate.CarUxRestrictions;
4import android.view.View;
5
6import androidx.annotation.CallSuper;
7import androidx.annotation.Nullable;
8import androidx.annotation.StyleRes;
9import androidx.car.R;
10import androidx.recyclerview.widget.RecyclerView;
11
12import java.util.ArrayList;
13import java.util.List;
14import java.util.function.Function;
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    public 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     * Does the work that moves the ListItem from dirty state to clean state, i.e. the work required
139     * the first time this ListItem {@code bind}s to {@link ListItem.ViewHolder}.
140     * This method will transition ListItem to clean state. ListItem in clean state should move to
141     * dirty state when it is modified by calling {@link #markDirty()}.
142     */
143    protected abstract void resolveDirtyState();
144
145    /**
146     * Binds this ListItem to {@code viewHolder} by applying data in ListItem to sub-views.
147     * Assume {@link ViewHolder#cleanUp()} has already been invoked.
148     */
149    protected abstract void onBind(VH viewHolder);
150
151    /**
152     * Same as {@link #addViewBinder(ViewBinder, ViewBinder)} when {@code cleanUp} ViewBinder
153     * is null.
154     *
155     * @param binder to interact with subviews in {@code ViewHolder}.
156     *
157     * @see #addViewBinder(ViewBinder, ViewBinder)
158     */
159    public final void addViewBinder(ViewBinder<VH> binder) {
160        addViewBinder(binder, null);
161    }
162
163    /**
164     * Adds {@link ViewBinder} to interact with sub-views in {@link ViewHolder}. These ViewBinders
165     * will always be applied after {@link #onBind(ViewHolder)}.
166     *
167     * <p>To interact with a foobar sub-view in {@code ViewHolder}, make sure to first set its
168     * visibility, or call setFoobar() setter method.
169     *
170     * <p>Example:
171     * <pre>
172     * {@code
173     * TextListItem item = new TextListItem(context);
174     * item.setTitle("title");
175     * item.addViewBinder((viewHolder) -> {
176     *     viewHolder.getTitle().doFoobar();
177     * }, (viewHolder) -> {
178     *     viewHolder.getTitle().revertFoobar();
179     * });
180     * }
181     * </pre>
182     *
183     * @param binder to interact with subviews in {@code ViewHolder}.
184     * @param cleanUp view binder to revert the effect of {@code binder}. cleanUp binders will be
185     *                 stored in {@link ListItem.ViewHolder} and should be invoked via
186     *                 {@link ViewHolder#cleanUp()} before {@code ViewHolder} is recycled.
187     *                 This is to avoid changed made to ViewHolder lingers around when ViewHolder is
188     *                 recycled. Pass in null to skip.
189     */
190    public final void addViewBinder(ViewBinder<VH> binder, @Nullable ViewBinder<VH> cleanUp) {
191        mCustomBinders.add(binder);
192        if (cleanUp != null) {
193            mCustomBinderCleanUps.add(cleanUp);
194        }
195        markDirty();
196    }
197
198    /**
199     * Removes the first occurrence of the specified item.
200     *
201     * @param binder to be removed.
202     * @return {@code true} if {@code binder} exists. {@code false} otherwise.
203     */
204    public boolean removeViewBinder(ViewBinder<VH> binder) {
205        return mCustomBinders.remove(binder);
206    }
207
208    /**
209     * Functional interface to provide a way to interact with views in {@code ViewHolder}.
210     * {@code ListItem} calls all added ViewBinders when it {@code bind}s to {@code ViewHolder}.
211     *
212     * @param  class that extends {@link RecyclerView.ViewHolder}.
213     */
214    public interface ViewBinder<VH> {
215        /**
216         * Provides a way to interact with views in view holder.
217         */
218        void bind(VH viewHolder);
219    }
220
221    /**
222     * ViewHolder that supports {@link ViewBinder}.
223     */
224    public abstract static class ViewHolder extends RecyclerView.ViewHolder {
225        private final List<ViewBinder> mCleanUps = new ArrayList<>();
226
227        public ViewHolder(View itemView) {
228            super(itemView);
229        }
230
231        /**
232         * Removes customization from previous ListItem. Intended to be used when this ViewHolder is
233         * bound to a ListItem.
234         */
235        public final void cleanUp() {
236            for (ViewBinder binder : mCleanUps) {
237                binder.bind(this);
238            }
239        }
240
241        /**
242         * Stores clean up ViewBinders that will be called in {@code cleanUp()}.
243         */
244        public final void addCleanUp(@Nullable ViewBinder<ViewHolder> cleanUp) {
245            if (cleanUp != null) {
246                mCleanUps.add(cleanUp);
247            }
248        }
249
250        /**
251         * Update children views to comply with UX restriction changes.
252         *
253         * @param restrictions current car UX restrictions.
254         */
255        protected abstract void complyWithUxRestrictions(CarUxRestrictions restrictions);
256    }
257}
258