[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);