[automerger] Support clearing up custom view binders by: am: 20e9f85df9

Change-Id: I8854e821bb924599fb4f605530d1ee82b7785830
diff --git a/car/src/androidTest/java/androidx/car/widget/TextListItemTest.java b/car/src/androidTest/java/androidx/car/widget/TextListItemTest.java
index 529a9f0..97706a0 100644
--- a/car/src/androidTest/java/androidx/car/widget/TextListItemTest.java
+++ b/car/src/androidTest/java/androidx/car/widget/TextListItemTest.java
@@ -38,6 +38,7 @@
 import android.support.test.rule.ActivityTestRule;
 import android.support.test.runner.AndroidJUnit4;
 import android.support.v7.widget.LinearLayoutManager;
+import android.text.TextUtils;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.RelativeLayout;
@@ -587,6 +588,43 @@
     }
 
     @Test
+    public void testRevertingViewBinder() throws Throwable {
+        TextListItem item0 = new TextListItem(mActivity);
+        item0.setBody("one item");
+        item0.addViewBinder(
+                (viewHolder) -> viewHolder.getBody().setEllipsize(TextUtils.TruncateAt.END),
+                (viewHolder -> viewHolder.getBody().setEllipsize(null)));
+
+        List<TextListItem> items = Arrays.asList(item0);
+        setupPagedListView(items);
+
+        TextListItem.ViewHolder viewHolder = getViewHolderAtPosition(0);
+
+        // Bind view holder to a new item - the customization made by item0 should be reverted.
+        TextListItem item1 = new TextListItem(mActivity);
+        item1.setBody("new item");
+        mActivityRule.runOnUiThread(() -> item1.bind(viewHolder));
+
+        assertThat(viewHolder.getBody().getEllipsize(), is(equalTo(null)));
+    }
+
+    @Test
+    public void testRemovingViewBinder() {
+        TextListItem item0 = new TextListItem(mActivity);
+        item0.setBody("one item");
+        ListItem.ViewBinder<TextListItem.ViewHolder> binder =
+                (viewHolder) -> viewHolder.getTitle().setEllipsize(TextUtils.TruncateAt.END);
+        item0.addViewBinder(binder);
+
+        assertTrue(item0.removeViewBinder(binder));
+
+        List<TextListItem> items = Arrays.asList(item0);
+        setupPagedListView(items);
+
+        assertThat(getViewHolderAtPosition(0).getBody().getEllipsize(), is(equalTo(null)));
+    }
+
+    @Test
     public void testSettingTitleOrBodyAsPrimaryText() {
         // Create 2 items, one with Title as primary (default) and one with Body.
         // The primary text, regardless of view, should have consistent look (as primary).
diff --git a/car/src/main/java/androidx/car/widget/ListItem.java b/car/src/main/java/androidx/car/widget/ListItem.java
index 74ff2dd..97e8fca 100644
--- a/car/src/main/java/androidx/car/widget/ListItem.java
+++ b/car/src/main/java/androidx/car/widget/ListItem.java
@@ -1,15 +1,19 @@
 package androidx.car.widget;
 
+import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView;
+import android.view.View;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.function.Function;
 
 /**
  * Definition of items that can be inserted into {@link ListItemAdapter}.
  *
- * @param <VH> ViewHolder.
+ * @param <VH> ViewHolder that extends {@link ListItem.ViewHolder}.
  */
-public abstract class ListItem<VH extends RecyclerView.ViewHolder> {
+public abstract class ListItem<VH extends ListItem.ViewHolder> {
 
     // Whether the item should calculate view layout params. This usually happens when the item is
     // updated after bind() is called. Calling bind() resets to false.
@@ -18,6 +22,11 @@
     // Tag for indicating whether to hide the divider.
     private boolean mHideDivider;
 
+    private final List<ViewBinder<VH>> mCustomBinders = new ArrayList<>();
+    // Stores ViewBinders to revert customization. Does not guarantee to 1:1 match ViewBinders
+    // in mCustomerBinders.
+    private final List<ViewBinder<VH>> mCustomBinderCleanUps = new ArrayList<>();
+
     /**
      * Classes that extends {@code ListItem} should register its view type in
      * {@link ListItemAdapter#registerListItemViewType(int, int, Function)}.
@@ -29,27 +38,44 @@
     /**
      * Called when ListItem is bound to its ViewHolder.
      */
-    public abstract void bind(VH viewHolder);
+    final void bind(VH viewHolder) {
+        // Attempt to clean up custom view binder from previous item (if any).
+        // Then save the clean up binders for next item.
+        viewHolder.cleanUp();
+        for (ViewBinder cleanUp : mCustomBinderCleanUps) {
+            viewHolder.addCleanUp(cleanUp);
+        }
+
+        if (isDirty()) {
+            resolveDirtyState();
+            markClean();
+        }
+        onBind(viewHolder);
+
+        // Custom view binders are applied after view layout.
+        for (ViewBinder<VH> binder: mCustomBinders) {
+            binder.bind(viewHolder);
+        }
+    }
 
     /**
-     * Marks this item so that sub-views in ViewHolder will need layout params re-calculated
-     * in next bind().
+     * Marks this item as dirty so {@link #resolveDirtyState()} is required in next bind() call.
      *
-     * This method should be called in each setter.
+     * <p>This method should be called in each setter.
      */
     protected void markDirty() {
         mDirty = true;
     }
 
     /**
-     * Marks this item as not dirty - no need to calculate sub-view layout params in bind().
+     * Marks this item as not dirty. No need to call {@link #resolveDirtyState()} in next bind().
      */
     protected void markClean() {
         mDirty = false;
     }
 
     /**
-     * @return {@code true} if this item needs to calculate sub-view layout params.
+     * @return {@code true} if next bind() should call {@link #resolveDirtyState()}.
      */
     protected boolean isDirty() {
         return mDirty;
@@ -75,16 +101,118 @@
         return mHideDivider;
     };
 
+
+    /**
+     * Does the work that moves the ListItem from dirty state to clean state, i.e. the work required
+     * the first time this ListItem {@code bind}s to {@link ListItem.ViewHolder}.
+     * This method will transition ListItem to clean state. ListItem in clean state should move to
+     * dirty state when it is modified by calling {@link #markDirty()}.
+     */
+    protected abstract void resolveDirtyState();
+
+    /**
+     * Binds this ListItem to {@code viewHolder} by applying data in ListItem to sub-views.
+     * Assume {@link ViewHolder#cleanUp()} has already been invoked.
+     */
+    protected abstract void onBind(VH viewHolder);
+
+    /**
+     * Same as {@link #addViewBinder(ViewBinder, ViewBinder)} when {@code cleanUp} ViewBinder
+     * is null.
+     *
+     * @param binder to interact with subviews in {@code ViewHolder}.
+     *
+     * @see #addViewBinder(ViewBinder, ViewBinder)
+     */
+    public final void addViewBinder(ViewBinder<VH> binder) {
+        addViewBinder(binder, null);
+    }
+
+    /**
+     * Adds {@link ViewBinder} to interact with sub-views in {@link ViewHolder}. These ViewBinders
+     * will always be applied after {@link #onBind(ViewHolder)}.
+     *
+     * <p>To interact with a foobar sub-view in {@code ViewHolder}, make sure to first set its
+     * visibility, or call setFoobar() setter method.
+     *
+     * <p>Example:
+     * <pre>
+     * {@code
+     * TextListItem item = new TextListItem(context);
+     * item.setTitle("title");
+     * item.addViewBinder((viewHolder) -> {
+     *     viewHolder.getTitle().doFoobar();
+     * }, (viewHolder) -> {
+     *     viewHolder.getTitle().revertFoobar();
+     * });
+     * }
+     * </pre>
+     *
+     * @params binder to interact with subviews in {@code ViewHolder}.
+     * @params cleanUp view binder to revert the effect of {@code binder}. cleanUp binders will be
+     *                 stored in {@link ListItem.ViewHolder} and should be invoked via
+     *                 {@link ViewHolder#cleanUp()} before {@code ViewHolder} is recycled.
+     *                 This is to avoid changed made to ViewHolder lingers around when ViewHolder is
+     *                 recycled. Pass in null to skip.
+     */
+    public final void addViewBinder(ViewBinder<VH> binder, @Nullable ViewBinder<VH> cleanUp) {
+        mCustomBinders.add(binder);
+        if (cleanUp != null) {
+            mCustomBinderCleanUps.add(cleanUp);
+        }
+        markDirty();
+    }
+
+    /**
+     * Removes the first occurrence of the specified item.
+     *
+     * @param binder to be removed.
+     * @return {@code true} if {@code binder} exists. {@code false} otherwise.
+     */
+    public boolean removeViewBinder(ViewBinder<VH> binder) {
+        return mCustomBinders.remove(binder);
+    }
+
     /**
      * Functional interface to provide a way to interact with views in {@code ViewHolder}.
      * {@code ListItem} calls all added ViewBinders when it {@code bind}s to {@code ViewHolder}.
      *
-     * @param <VH> extends {@link RecyclerView.ViewHolder}.
+     * @param <VH> class that extends {@link RecyclerView.ViewHolder}.
      */
-    public interface ViewBinder<VH extends RecyclerView.ViewHolder> {
+    public interface ViewBinder<VH> {
         /**
          * Provides a way to interact with views in view holder.
          */
         void bind(VH viewHolder);
     }
+
+    /**
+     * ViewHolder that supports {@link ViewBinder}.
+     */
+    public abstract static class ViewHolder extends RecyclerView.ViewHolder {
+        private final List<ViewBinder> mCleanUps = new ArrayList<>();
+
+        public ViewHolder(View itemView) {
+            super(itemView);
+        }
+
+        /**
+         * Removes customization from previous ListItem. Intended to be used when this ViewHolder is
+         * bound to a ListItem.
+         */
+        public final void cleanUp() {
+            for (ViewBinder binder : mCleanUps) {
+                binder.bind(this);
+            }
+        }
+
+        /**
+         * Stores clean up ViewBinders that will be called in {@code cleanUp()}.
+         */
+        public final void addCleanUp(@Nullable ViewBinder<ViewHolder> cleanUp) {
+            if (cleanUp != null) {
+                mCleanUps.add(cleanUp);
+            }
+        }
+    }
 }
diff --git a/car/src/main/java/androidx/car/widget/ListItemAdapter.java b/car/src/main/java/androidx/car/widget/ListItemAdapter.java
index 8dedbf5..78b1d6e 100644
--- a/car/src/main/java/androidx/car/widget/ListItemAdapter.java
+++ b/car/src/main/java/androidx/car/widget/ListItemAdapter.java
@@ -43,7 +43,7 @@
  *
  */
 public class ListItemAdapter extends
-        RecyclerView.Adapter<RecyclerView.ViewHolder> implements PagedListView.ItemCap,
+        RecyclerView.Adapter<ListItem.ViewHolder> implements PagedListView.ItemCap,
         PagedListView.DividerVisibilityManager {
 
     /**
@@ -74,7 +74,7 @@
     static final int LIST_ITEM_TYPE_SEEKBAR = 2;
 
     private final SparseIntArray mViewHolderLayoutResIds = new SparseIntArray();
-    private final SparseArray<Function<View, RecyclerView.ViewHolder>> mViewHolderCreator =
+    private final SparseArray<Function<View, ListItem.ViewHolder>> mViewHolderCreator =
             new SparseArray<>();
 
     /**
@@ -90,7 +90,7 @@
      * @param function function to create ViewHolder for {@code viewType}.
      */
     public void registerListItemViewType(int viewType, @LayoutRes int layoutResId,
-            Function<View, RecyclerView.ViewHolder> function) {
+            Function<View, ListItem.ViewHolder> function) {
         if (mViewHolderLayoutResIds.get(viewType) != 0
                 || mViewHolderCreator.get(viewType) != null) {
             throw new IllegalArgumentException("View type is already registered.");
@@ -121,7 +121,7 @@
     }
 
     @Override
-    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+    public ListItem.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         if (mViewHolderLayoutResIds.get(viewType) == 0
                 || mViewHolderCreator.get(viewType) == null) {
             throw new IllegalArgumentException("Unregistered view type.");
@@ -175,7 +175,7 @@
     }
 
     @Override
-    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+    public void onBindViewHolder(ListItem.ViewHolder holder, int position) {
         ListItem item = mItemProvider.get(position);
         item.bind(holder);
 
diff --git a/car/src/main/java/androidx/car/widget/SeekbarListItem.java b/car/src/main/java/androidx/car/widget/SeekbarListItem.java
index 54c37ef..2e98817 100644
--- a/car/src/main/java/androidx/car/widget/SeekbarListItem.java
+++ b/car/src/main/java/androidx/car/widget/SeekbarListItem.java
@@ -24,7 +24,6 @@
 import android.support.annotation.IdRes;
 import android.support.annotation.IntDef;
 import android.support.annotation.Nullable;
-import android.support.v7.widget.RecyclerView;
 import android.text.TextUtils;
 import android.view.View;
 import android.view.ViewGroup;
@@ -93,8 +92,6 @@
     private final Context mContext;
 
     private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();
-    // Store custom binders separately so they will bind after binders created by setters.
-    private final List<ViewBinder<ViewHolder>> mCustomBinders = new ArrayList<>();
 
     @PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
     private int mPrimaryActionIconResId;
@@ -156,33 +153,32 @@
     }
 
     /**
-     * Applies all {@link ViewBinder} to {@code ViewHolder}.
+     * Calculates the layout params for views in {@link ViewHolder}.
      */
     @Override
-    public void bind(ViewHolder viewHolder) {
-        if (isDirty()) {
-            mBinders.clear();
+    protected void resolveDirtyState() {
+        mBinders.clear();
 
-            // Create binders that adjust layout params of each view.
-            setItemLayoutHeight();
-            setPrimaryAction();
-            setSeekBarAndText();
-            setSupplementalAction();
+        // Create binders that adjust layout params of each view.
+        setItemLayoutHeight();
+        setPrimaryAction();
+        setSeekBarAndText();
+        setSupplementalAction();
+    }
 
-            // Custom view binders are always applied after the one created by this class.
-            mBinders.addAll(mCustomBinders);
-
-            markClean();
-        }
-
+    /**
+     * Hides all views in {@link ViewHolder} then applies ViewBinders to adjust view layout params.
+     */
+    @Override
+    protected void onBind(ViewHolder viewHolder) {
         // Hide all subviews then apply view binders to adjust subviews.
-        setSubViewsGone(viewHolder);
+        hideSubViews(viewHolder);
         for (ViewBinder binder : mBinders) {
             binder.bind(viewHolder);
         }
     }
 
-    private void setSubViewsGone(ViewHolder vh) {
+    private void hideSubViews(ViewHolder vh) {
         View[] subviews = new View[] {
                 vh.getPrimaryIcon(),
                 // SeekBar is always visible.
@@ -499,31 +495,9 @@
     }
 
     /**
-     * Adds {@code ViewBinder} to interact with sub-views in {@link ViewHolder}. These ViewBinders
-     * will always be applied after other {@code setFoobar} methods have bound.
-     *
-     * <p>Make sure to call setFoobar() method on the intended sub-view first.
-     *
-     * <p>Example:
-     * <pre>
-     * {@code
-     * SeekbarListItem item = new SeebarListItem(context);
-     * item.setPrimaryActionIcon(R.drawable.icon);
-     * item.addViewBinder((viewHolder) -> {
-     *     viewHolder.getPrimaryIcon().doMoreStuff();
-     * });
-     * }
-     * </pre>
-     */
-    public void addViewBinder(ViewBinder<ViewHolder> viewBinder) {
-        mCustomBinders.add(viewBinder);
-        markDirty();
-    }
-
-    /**
      * Holds views of SeekbarListItem.
      */
-    public static class ViewHolder extends RecyclerView.ViewHolder {
+    public static class ViewHolder extends ListItem.ViewHolder {
 
         private RelativeLayout mContainerLayout;
 
diff --git a/car/src/main/java/androidx/car/widget/TextListItem.java b/car/src/main/java/androidx/car/widget/TextListItem.java
index 71b6c96..871c14b 100644
--- a/car/src/main/java/androidx/car/widget/TextListItem.java
+++ b/car/src/main/java/androidx/car/widget/TextListItem.java
@@ -101,8 +101,6 @@
     private final Context mContext;
 
     private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();
-    // Store custom binders separately so they will bind after binders created by setters.
-    private final List<ViewBinder<ViewHolder>> mCustomBinders = new ArrayList<>();
 
     private View.OnClickListener mOnClickListener;
 
@@ -151,34 +149,32 @@
     }
 
     /**
-     * Applies all {@link ViewBinder} to {@code ViewHolder}.
+     * Calculates the layout params for views in {@link ViewHolder}.
      */
     @Override
-    public void bind(ViewHolder viewHolder) {
-        if (isDirty()) {
-            mBinders.clear();
+    protected void resolveDirtyState() {
+        mBinders.clear();
 
-            // Create binders that adjust layout params of each view.
-            setItemLayoutHeight();
-            setPrimaryAction();
-            setText();
-            setSupplementalActions();
-            setOnClickListener();
+        // Create binders that adjust layout params of each view.
+        setItemLayoutHeight();
+        setPrimaryAction();
+        setText();
+        setSupplementalActions();
+        setOnClickListener();
+    }
 
-            // Custom view binders are always applied after the one created by this class.
-            mBinders.addAll(mCustomBinders);
-
-            markClean();
-        }
-
-        // Hide all subviews then apply view binders to adjust subviews.
-        setAllSubViewsGone(viewHolder);
+    /**
+     * Hides all views in {@link ViewHolder} then applies ViewBinders to adjust view layout params.
+     */
+    @Override
+    public void onBind(ViewHolder viewHolder) {
+        hideSubViews(viewHolder);
         for (ViewBinder binder : mBinders) {
             binder.bind(viewHolder);
         }
     }
 
-    void setAllSubViewsGone(ViewHolder vh) {
+    private void hideSubViews(ViewHolder vh) {
         View[] subviews = new View[] {
                 vh.getPrimaryIcon(),
                 vh.getTitle(), vh.getBody(),
@@ -709,31 +705,9 @@
     }
 
     /**
-     * Adds {@link ViewBinder} to interact with sub-views in {@link ViewHolder}. These ViewBinders
-     * will always bind after other {@code setFoobar} methods have bound.
-     *
-     * <p>Make sure to call setFoobar() method on the intended sub-view first.
-     *
-     * <p>Example:
-     * <pre>
-     * {@code
-     * TextListItem item = new TextListItem(context);
-     * item.setTitle("title");
-     * item.addViewBinder((viewHolder) -> {
-     *     viewHolder.getTitle().doMoreStuff();
-     * });
-     * }
-     * </pre>
-     */
-    public void addViewBinder(ViewBinder<ViewHolder> binder) {
-        mCustomBinders.add(binder);
-        markDirty();
-    }
-
-    /**
      * Holds views of TextListItem.
      */
-    public static class ViewHolder extends RecyclerView.ViewHolder {
+    public static class ViewHolder extends ListItem.ViewHolder {
 
         private RelativeLayout mContainerLayout;
 
diff --git a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SeekbarListItemActivity.java b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SeekbarListItemActivity.java
index 198f092..6975d61 100644
--- a/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SeekbarListItemActivity.java
+++ b/samples/SupportCarDemos/src/main/java/com/example/androidx/car/SeekbarListItemActivity.java
@@ -18,7 +18,6 @@
 
 import android.app.Activity;
 import android.content.Context;
-import android.graphics.Color;
 import android.os.Bundle;
 import android.widget.SeekBar;
 import android.widget.Toast;
@@ -40,17 +39,12 @@
 
     PagedListView mPagedListView;
 
-    private static int pixelToDip(Context context, int pixels) {
-        return (int) (pixels / context.getResources().getDisplayMetrics().density);
-    }
-
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_paged_list_view);
 
         mPagedListView = findViewById(R.id.paged_list_view);
-        mPagedListView.setBackgroundColor(Color.BLUE);
 
         ListItemAdapter adapter = new ListItemAdapter(this,
                 new SampleProvider(this), ListItemAdapter.BackgroundStyle.PANEL);