Skip to content

Commit

Permalink
OnModelCheckedChangeListener (#725)
Browse files Browse the repository at this point in the history
* added OnModelCheckedChangeListener with tests

* created ListenerUtils and moved common functions in it.
  • Loading branch information
KirkBushman authored and elihart committed Apr 10, 2019
1 parent 688bddb commit b0aa309
Show file tree
Hide file tree
Showing 16 changed files with 585 additions and 45 deletions.
48 changes: 48 additions & 0 deletions epoxy-adapter/src/main/java/com/airbnb/epoxy/ListenersUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.airbnb.epoxy;

import android.view.View;
import android.view.ViewParent;

import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;

public class ListenersUtils {

@Nullable
static EpoxyViewHolder getEpoxyHolderForChildView(View v) {
RecyclerView recyclerView = findParentRecyclerView(v);
if (recyclerView == null) {
return null;
}

ViewHolder viewHolder = recyclerView.findContainingViewHolder(v);
if (viewHolder == null) {
return null;
}

if (!(viewHolder instanceof EpoxyViewHolder)) {
return null;
}

return (EpoxyViewHolder) viewHolder;
}

@Nullable
private static RecyclerView findParentRecyclerView(@Nullable View v) {
if (v == null) {
return null;
}

ViewParent parent = v.getParent();
if (parent instanceof RecyclerView) {
return (RecyclerView) parent;
}

if (parent instanceof View) {
return findParentRecyclerView((View) parent);
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.airbnb.epoxy;

import android.widget.CompoundButton;

public interface OnModelCheckedChangeListener<T extends EpoxyModel<?>, V> {
/**
* Called when the view bound to the model is checked.
*
* @param model The model that the view is bound to.
* @param parentView The view bound to the model which received the click.
* @param clickedView The view that received the click. This is either a child of the parentView
* or the parentView itself
* @param isChecked The new value for isChecked property.
* @param position The position of the model in the adapter.
*/
void onChecked(T model, V parentView,
CompoundButton checkedView, boolean isChecked, int position);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.airbnb.epoxy;

import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;

import androidx.recyclerview.widget.RecyclerView;

/**
* Used in the generated models to transform normal checked change listener to model
* checked change.
*/
public class WrappedEpoxyModelCheckedChangeListener<T extends EpoxyModel<?>, V>
implements OnCheckedChangeListener {

private final OnModelCheckedChangeListener<T, V> originalCheckedChangeListener;

public WrappedEpoxyModelCheckedChangeListener(
OnModelCheckedChangeListener<T, V> checkedListener
) {
if (checkedListener == null) {
throw new IllegalArgumentException("Checked change listener cannot be null");
}

this.originalCheckedChangeListener = checkedListener;
}

@Override
public void onCheckedChanged(CompoundButton button, boolean isChecked) {
EpoxyViewHolder epoxyHolder = ListenersUtils.getEpoxyHolderForChildView(button);
if (epoxyHolder == null) {
throw new IllegalStateException("Could not find RecyclerView holder for clicked view");
}

final int adapterPosition = epoxyHolder.getAdapterPosition();
if (adapterPosition != RecyclerView.NO_POSITION) {
originalCheckedChangeListener
.onChecked((T) epoxyHolder.getModel(), (V) epoxyHolder.objectToBind(), button,
isChecked, adapterPosition);
}
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof WrappedEpoxyModelCheckedChangeListener)) {
return false;
}

WrappedEpoxyModelCheckedChangeListener<?, ?>
that = (WrappedEpoxyModelCheckedChangeListener<?, ?>) o;

return originalCheckedChangeListener.equals(that.originalCheckedChangeListener);
}

@Override
public int hashCode() {
return originalCheckedChangeListener.hashCode();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewParent;

import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;

/**
* Used in the generated models to transform normal view click listeners to model click
Expand Down Expand Up @@ -40,7 +37,7 @@ public WrappedEpoxyModelClickListener(OnModelLongClickListener<T, V> clickListen

@Override
public void onClick(View v) {
EpoxyViewHolder epoxyHolder = getEpoxyHolderForChildView(v);
EpoxyViewHolder epoxyHolder = ListenersUtils.getEpoxyHolderForChildView(v);
if (epoxyHolder == null) {
throw new IllegalStateException("Could not find RecyclerView holder for clicked view");
}
Expand All @@ -55,7 +52,7 @@ public void onClick(View v) {

@Override
public boolean onLongClick(View v) {
EpoxyViewHolder epoxyHolder = getEpoxyHolderForChildView(v);
EpoxyViewHolder epoxyHolder = ListenersUtils.getEpoxyHolderForChildView(v);
if (epoxyHolder == null) {
throw new IllegalStateException("Could not find RecyclerView holder for clicked view");
}
Expand All @@ -71,43 +68,6 @@ public boolean onLongClick(View v) {
return false;
}

@Nullable
private static EpoxyViewHolder getEpoxyHolderForChildView(View v) {
RecyclerView recyclerView = findParentRecyclerView(v);
if (recyclerView == null) {
return null;
}

ViewHolder viewHolder = recyclerView.findContainingViewHolder(v);
if (viewHolder == null) {
return null;
}

if (!(viewHolder instanceof EpoxyViewHolder)) {
return null;
}

return (EpoxyViewHolder) viewHolder;
}

@Nullable
private static RecyclerView findParentRecyclerView(@Nullable View v) {
if (v == null) {
return null;
}

ViewParent parent = v.getParent();
if (parent instanceof RecyclerView) {
return (RecyclerView) parent;
}

if (parent instanceof View) {
return findParentRecyclerView((View) parent);
}

return null;
}

@Override
public boolean equals(Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.airbnb.epoxy.integrationtest;

import android.view.View;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;

import com.airbnb.epoxy.EpoxyAttribute;
import com.airbnb.epoxy.EpoxyModel;

import androidx.annotation.NonNull;

public class ModelWithCheckedChangeListener extends EpoxyModel<View> {

@EpoxyAttribute OnCheckedChangeListener checkedChangeListener;

@Override
protected int getDefaultLayout() {
return R.layout.model_with_checked_change;
}

@Override
public void bind(@NonNull View view) {
if (view instanceof CompoundButton) {
((CompoundButton) view).setOnCheckedChangeListener(checkedChangeListener);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">

</CheckBox>
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class BindDiffTest {

@Test
fun propGroupChangedFromOneAttributeToAnother() {
val clickListener = View.OnClickListener { v -> }
val clickListener = View.OnClickListener {}
validateDiff(
model1Props = {
requiredText("hello")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import android.view.View;
import android.view.View.OnClickListener;
import android.widget.CompoundButton;

import com.airbnb.epoxy.integrationtest.BuildConfig;
import com.airbnb.epoxy.integrationtest.ModelWithCheckedChangeListener_;
import com.airbnb.epoxy.integrationtest.ModelWithClickListener_;
import com.airbnb.epoxy.integrationtest.ModelWithLongClickListener_;

Expand Down Expand Up @@ -69,6 +71,17 @@ public boolean onLongClick(ModelWithLongClickListener_ model, View view, View v,
}
}

static class ModelCheckedChangeListener
implements OnModelCheckedChangeListener<ModelWithCheckedChangeListener_, View> {
boolean checked;

@Override
public void onChecked(ModelWithCheckedChangeListener_ model, View parentView,
CompoundButton checkedView, boolean isChecked, int position) {
checked = true;
}
}

static class ViewClickListener implements OnClickListener {
boolean clicked;

Expand Down Expand Up @@ -137,6 +150,46 @@ public void basicModelLongClickListener() {
verify(modelClickListener).onLongClick(eq(model), any(View.class), eq(viewMock), eq(1));
}

@Test
public void basicModelCheckedChangeListener() {
final ModelWithCheckedChangeListener_ model = new ModelWithCheckedChangeListener_();
ModelCheckedChangeListener modelCheckedChangeListener = spy(new ModelCheckedChangeListener());
model.checkedChangeListener(modelCheckedChangeListener);

TestController controller = new TestController();
controller.setModel(model);

lifecycleHelper.buildModelsAndBind(controller);

CompoundButton compoundMock = mockCompoundButtonForClicking(model);

model.checkedChangeListener().onCheckedChanged(compoundMock, true);
assertTrue(modelCheckedChangeListener.checked);

verify(modelCheckedChangeListener).onChecked(eq(model), any(View.class), any(CompoundButton.class), eq(true), eq(1));
}

private CompoundButton mockCompoundButtonForClicking(EpoxyModel model) {
CompoundButton mockedView = mock(CompoundButton.class);
RecyclerView recyclerMock = mock(RecyclerView.class);
EpoxyViewHolder holderMock = mock(EpoxyViewHolder.class);

when(holderMock.getAdapterPosition()).thenReturn(1);
doReturn(recyclerMock).when(mockedView).getParent();
doReturn(holderMock).when(recyclerMock).findContainingViewHolder(mockedView);
doReturn(model).when(holderMock).getModel();

when(mockedView.getParent()).thenReturn(recyclerMock);
when(recyclerMock.findContainingViewHolder(mockedView)).thenReturn(holderMock);
when(holderMock.getAdapterPosition()).thenReturn(1);
when(holderMock.getModel()).thenReturn(model);

View parentView = mock(View.class);
when(holderMock.objectToBind()).thenReturn(parentView);
doReturn(parentView).when(holderMock).objectToBind();
return mockedView;
}

@Test
public void modelClickListenerOverridesViewClickListener() {
final ModelWithClickListener_ model = new ModelWithClickListener_();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.airbnb.epoxy
import com.airbnb.epoxy.GeneratedModelInfo.AttributeGroup
import com.airbnb.epoxy.Utils.EPOXY_MODEL_TYPE
import com.airbnb.epoxy.Utils.isSubtypeOfType
import com.airbnb.epoxy.Utils.isViewCheckedChangeListenerType
import com.airbnb.epoxy.Utils.isViewClickListenerType
import com.airbnb.epoxy.Utils.isViewLongClickListenerType
import com.squareup.javapoet.AnnotationSpec
Expand Down Expand Up @@ -104,6 +105,9 @@ internal abstract class AttributeInfo {
val isViewLongClickListener: Boolean
get() = isViewLongClickListenerType(typeMirror)

val isViewCheckedChangeListener: Boolean
get() = isViewCheckedChangeListenerType(typeMirror)

val isBoolean: Boolean
get() = Utils.isBooleanType(typeMirror)

Expand Down
Loading

0 comments on commit b0aa309

Please sign in to comment.