Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OnModelCheckedChangeListener #725

Merged
merged 3 commits into from
Apr 10, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,102 @@
package com.airbnb.epoxy;

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

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

/**
* 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 = 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);
}
}

@Nullable
private static EpoxyViewHolder getEpoxyHolderForChildView(View v) {
KirkBushman marked this conversation as resolved.
Show resolved Hide resolved
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) {
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
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import com.airbnb.epoxy.ClassNames.PARIS_STYLE
import com.airbnb.epoxy.Utils.EPOXY_CONTROLLER_TYPE
import com.airbnb.epoxy.Utils.EPOXY_VIEW_HOLDER_TYPE
import com.airbnb.epoxy.Utils.GENERATED_MODEL_INTERFACE
import com.airbnb.epoxy.Utils.MODEL_CHECKED_CHANGE_LISTENER_TYPE
import com.airbnb.epoxy.Utils.MODEL_CLICK_LISTENER_TYPE
import com.airbnb.epoxy.Utils.MODEL_LONG_CLICK_LISTENER_TYPE
import com.airbnb.epoxy.Utils.ON_BIND_MODEL_LISTENER_TYPE
import com.airbnb.epoxy.Utils.ON_UNBIND_MODEL_LISTENER_TYPE
import com.airbnb.epoxy.Utils.ON_VISIBILITY_MODEL_LISTENER_TYPE
import com.airbnb.epoxy.Utils.ON_VISIBILITY_STATE_MODEL_LISTENER_TYPE
import com.airbnb.epoxy.Utils.UNTYPED_EPOXY_MODEL_TYPE
import com.airbnb.epoxy.Utils.WRAPPED_CHECKED_LISTENER_TYPE
import com.airbnb.epoxy.Utils.WRAPPED_LISTENER_TYPE
import com.airbnb.epoxy.Utils.getClassName
import com.airbnb.epoxy.Utils.implementsMethod
Expand Down Expand Up @@ -375,6 +377,16 @@ internal class GeneratedModelWriter(
)
}

private fun getModelCheckedChangeListenerType(
classInfo: GeneratedModelInfo
): ParameterizedTypeName {
return ParameterizedTypeName.get(
getClassName(MODEL_CHECKED_CHANGE_LISTENER_TYPE),
classInfo.parameterizedGeneratedName,
classInfo.modelType
)
}

/** Include any constructors that are in the super class. */
private fun generateConstructors(info: GeneratedModelInfo): Iterable<MethodSpec> {
return info.constructors.map {
Expand Down Expand Up @@ -1309,6 +1321,10 @@ internal class GeneratedModelWriter(
methods.add(generateSetClickModelListener(modelInfo, attr))
}

if (attr.isViewCheckedChangeListener) {
methods.add(generateSetCheckedChangeModelListener(modelInfo, attr))
}

if (attr.generateSetter && !attr.hasFinalModifier) {
methods.add(generateSetter(modelInfo, attr))
}
Expand Down Expand Up @@ -1366,6 +1382,46 @@ internal class GeneratedModelWriter(
return builder.build()
}

private fun generateSetCheckedChangeModelListener(
classInfo: GeneratedModelInfo,
attribute: AttributeInfo
): MethodSpec {
val attributeName = attribute.generatedSetterName()
val checkedListenerType = getModelCheckedChangeListenerType(classInfo)

val param = ParameterSpec.builder(checkedListenerType, attributeName, FINAL)
.addAnnotations(attribute.setterAnnotations).build()

val builder = MethodSpec.methodBuilder(attributeName)
.addJavadoc(
"Set a checked change listener that will provide the parent view, model, value, and adapter " +
"position of the checked view. This will clear the normal " +
"CompoundButton.OnCheckedChangeListener if one has been set"
)
.addModifiers(PUBLIC)
.returns(classInfo.parameterizedGeneratedName)
.addParameter(param)

setBitSetIfNeeded(classInfo, attribute, builder)

val wrapperCheckedListenerConstructor = CodeBlock.of(
"new \$T(\$L)",
getClassName(WRAPPED_CHECKED_LISTENER_TYPE),
param.name
)

addOnMutationCall(builder)
.beginControlFlow("if (\$L == null)", attributeName)
.addStatement(attribute.setterCode(), "null")
.endControlFlow()
.beginControlFlow("else")
.addStatement(attribute.setterCode(), wrapperCheckedListenerConstructor)
.endControlFlow()
.addStatement("return this")

return builder.build()
}

private fun generateEquals(helperClass: GeneratedModelInfo) = buildMethod("equals") {
addAnnotation(Override::class.java)
addModifiers(PUBLIC)
Expand Down
10 changes: 10 additions & 0 deletions epoxy-processor/src/main/java/com/airbnb/epoxy/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,19 @@ class Utils {
static final String EPOXY_CONTROLLER_TYPE = "com.airbnb.epoxy.EpoxyController";
static final String VIEW_CLICK_LISTENER_TYPE = "android.view.View.OnClickListener";
static final String VIEW_LONG_CLICK_LISTENER_TYPE = "android.view.View.OnLongClickListener";
static final String VIEW_CHECKED_CHANGE_LISTENER_TYPE =
"android.widget.CompoundButton.OnCheckedChangeListener";
static final String DRAWABLE_TYPE = "android.graphics.drawable.Drawable";
static final String GENERATED_MODEL_INTERFACE = "com.airbnb.epoxy.GeneratedModel";
static final String MODEL_CLICK_LISTENER_TYPE = "com.airbnb.epoxy.OnModelClickListener";
static final String MODEL_LONG_CLICK_LISTENER_TYPE = "com.airbnb.epoxy.OnModelLongClickListener";
static final String MODEL_CHECKED_CHANGE_LISTENER_TYPE =
"com.airbnb.epoxy.OnModelCheckedChangeListener";
static final String ON_BIND_MODEL_LISTENER_TYPE = "com.airbnb.epoxy.OnModelBoundListener";
static final String ON_UNBIND_MODEL_LISTENER_TYPE = "com.airbnb.epoxy.OnModelUnboundListener";
static final String WRAPPED_LISTENER_TYPE = "com.airbnb.epoxy.WrappedEpoxyModelClickListener";
static final String WRAPPED_CHECKED_LISTENER_TYPE =
"com.airbnb.epoxy.WrappedEpoxyModelCheckedChangeListener";
static final String DATA_BINDING_MODEL_TYPE = "com.airbnb.epoxy.DataBindingEpoxyModel";
static final String ON_VISIBILITY_STATE_MODEL_LISTENER_TYPE =
"com.airbnb.epoxy.OnModelVisibilityStateChangedListener";
Expand Down Expand Up @@ -119,6 +125,10 @@ static boolean isViewLongClickListenerType(TypeMirror type) {
return isType(type, VIEW_LONG_CLICK_LISTENER_TYPE);
}

static boolean isViewCheckedChangeListenerType(TypeMirror type) {
return isType(type, VIEW_CHECKED_CHANGE_LISTENER_TYPE);
}

static boolean isBooleanType(TypeMirror type) {
return type.getKind() == TypeKind.BOOLEAN || Utils.isType(type, "java.lang.Boolean");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ public void modelWithViewClickLongListener() {
.generatesSources(generatedNoLayoutModel);
}

@Test
public void modelWithCheckedChangeListener() {
assertGeneration("ModelWithCheckedChangeListener.java", "ModelWithCheckedChangeListener_.java");
}

@Test
public void testModelWithPrivateAttributeWithoutGetterAndSetterFails() {
assertGenerationError("ModelWithPrivateFieldWithoutGetterAndSetter.java",
Expand Down
Loading