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

Allow ModelView usage without an xml layout #282

Merged
merged 2 commits into from
Sep 13, 2017
Merged
Show file tree
Hide file tree
Changes from all 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
27 changes: 26 additions & 1 deletion epoxy-annotations/src/main/java/com/airbnb/epoxy/ModelView.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,34 @@
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface ModelView {

/**
* Use with {@link #autoLayout()} to declare what layout parameters should be used to size your
* view when it is added to a RecyclerView. This maps to the LayoutParams options {@code
* layout_width} and {@code layout_height}.
*/
enum Size {
NONE,
WRAP_WIDTH_WRAP_HEIGHT,
WRAP_WIDTH_MATCH_HEIGHT,
MATCH_WIDTH_WRAP_HEIGHT,
MATCH_WIDTH_MATCH_HEIGHT
}

/**
* If set to an option besides {@link Size#NONE} Epoxy will create an instance of this view
* programmatically at runtime instead of inflating the view from xml. This is an alternative to
* using {@link #defaultLayout()}, and is a good option if you just need to specify layout
* parameters on your view with no other styling.
* <p>
* The size option you choose will define which layout parameters Epoxy uses at runtime when
* creating the view.
*/
Size autoLayout() default Size.NONE;

/**
* The layout file to use in the generated model to inflate the view. This is required unless a
* default pattern is set via {@link PackageModelViewConfig}.
* default pattern is set via {@link PackageModelViewConfig} or {@link #autoLayout()} is used.
* <p>
* Overrides any default set in {@link PackageModelViewConfig}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ private ClassNames() {
static final ClassName ANDROID_CONTEXT = get(PKG_ANDROID_CONTENT, "Context");
static final ClassName ANDROID_VIEW = get(PKG_ANDROID_VIEW, "View");
static final ClassName ANDROID_VIEW_GROUP = get(PKG_ANDROID_VIEW, "ViewGroup");
static final ClassName ANDROID_MARGIN_LAYOUT_PARAMS =
get(PKG_ANDROID_VIEW, "ViewGroup", "MarginLayoutParams");
static final ClassName ANDROID_R = get(PKG_ANDROID, "R");

static final ClassName LITHO_COMPONENT = get(PKG_LITHO, "Component");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.support.annotation.Nullable;

import com.airbnb.epoxy.ModelView.Size;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
Expand Down Expand Up @@ -64,6 +65,12 @@ abstract class GeneratedModelInfo {
*/
private ParisStyleAttributeInfo styleBuilderInfo;

/**
* An option set via {@link ModelView#autoLayout()} to have Epoxy create the view programmatically
* instead of via xml layout resource inflation.
*/
Size layoutParams = Size.NONE;

/**
* Get information about methods returning class type of the original class so we can duplicate
* them in the generated class for chaining purposes
Expand Down Expand Up @@ -204,12 +211,16 @@ boolean isStyleable() {
return getStyleBuilderInfo() != null;
}

public void setStyleable(
void setStyleable(
@NotNull ParisStyleAttributeInfo parisStyleAttributeInfo) {
styleBuilderInfo = parisStyleAttributeInfo;
addAttribute(parisStyleAttributeInfo);
}

boolean isProgrammaticView() {
return isStyleable() || layoutParams != Size.NONE;
}

static class ConstructorInfo {
final Set<Modifier> modifiers;
final List<ParameterSpec> params;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ class GeneratedModelWriter {
private static final String GET_DEFAULT_LAYOUT_METHOD_NAME = "getDefaultLayout";
static final String ATTRIBUTES_BITSET_FIELD_NAME = "assignedAttributes" + GENERATED_FIELD_SUFFIX;

private static final CodeBlock LAYOUT_PARAMS_MATCH_PARENT =
CodeBlock.of("$T.MATCH_PARENT", ClassNames.ANDROID_MARGIN_LAYOUT_PARAMS);
private static final CodeBlock LAYOUT_PARAMS_WRAP_CONTENT =
CodeBlock.of("$T.WRAP_CONTENT", ClassNames.ANDROID_MARGIN_LAYOUT_PARAMS);

private final Filer filer;
private final Types typeUtils;
private final ErrorLogger errorLogger;
Expand Down Expand Up @@ -151,6 +156,7 @@ void generateClassForModel(GeneratedModelInfo info, BuilderHooks builderHooks)
generateDebugAddToMethodIfNeeded(builder, info);

builder
.addMethods(generateProgrammaticViewMethods(info))
.addMethods(generateBindMethods(info))
.addMethods(generateStyleableViewMethods(info))
.addMethods(generateSettersAndGetters(info))
Expand All @@ -175,7 +181,7 @@ void generateClassForModel(GeneratedModelInfo info, BuilderHooks builderHooks)

private Iterable<MethodSpec> generateOtherLayoutOptions(GeneratedModelInfo info) {
if (!info.includeOtherLayoutOptions
|| info.isStyleable()) { // Layout resources can't be mixed with programmatic styles
|| info.isProgrammaticView()) { // Layout resources can't be mixed with programmatic views
return Collections.emptyList();
}

Expand Down Expand Up @@ -410,6 +416,67 @@ private static int attributeIndex(GeneratedModelInfo modelInfo, AttributeInfo at
return index;
}

private Iterable<MethodSpec> generateProgrammaticViewMethods(GeneratedModelInfo modelInfo) {

if (!modelInfo.isProgrammaticView()) {
return Collections.emptyList();
}

List<MethodSpec> methods = new ArrayList<>();

// getViewType method so that view type is generated at runtime
methods.add(MethodSpec.methodBuilder("getViewType")
.addAnnotation(Override.class)
.addModifiers(PROTECTED)
.returns(TypeName.INT)
.addStatement("return 0", modelInfo.boundObjectTypeName)
.build());

// buildView method to return new view instance
Builder builder = MethodSpec.methodBuilder("buildView")
.addAnnotation(Override.class)
.addParameter(ClassNames.ANDROID_VIEW_GROUP, "parent")
.addModifiers(PROTECTED)
.returns(modelInfo.boundObjectTypeName)
.addStatement("$T v = new $T(parent.getContext())", modelInfo.boundObjectTypeName,
modelInfo.boundObjectTypeName);

CodeBlock layoutWidth;
CodeBlock layoutHeight;
switch (modelInfo.layoutParams) {
case WRAP_WIDTH_MATCH_HEIGHT:
layoutWidth = LAYOUT_PARAMS_WRAP_CONTENT;
layoutHeight = LAYOUT_PARAMS_MATCH_PARENT;
break;
case MATCH_WIDTH_MATCH_HEIGHT:
layoutWidth = LAYOUT_PARAMS_MATCH_PARENT;
layoutHeight = LAYOUT_PARAMS_MATCH_PARENT;
break;
case MATCH_WIDTH_WRAP_HEIGHT:
layoutWidth = LAYOUT_PARAMS_MATCH_PARENT;
layoutHeight = LAYOUT_PARAMS_WRAP_CONTENT;
break;
case WRAP_WIDTH_WRAP_HEIGHT:
default:
// This will be used for Styleable views as the default
layoutWidth = LAYOUT_PARAMS_WRAP_CONTENT;
layoutHeight = LAYOUT_PARAMS_WRAP_CONTENT;
}

builder
.addStatement("v.setLayoutParams(new $T($L, $L))",
ClassNames.ANDROID_MARGIN_LAYOUT_PARAMS, layoutWidth, layoutHeight);

ParisStyleAttributeInfo styleBuilderInfo = modelInfo.getStyleBuilderInfo();
if (styleBuilderInfo != null) {
addStyleApplierCode(builder, styleBuilderInfo, "v");
}

methods.add(builder.addStatement("return v").build());

return methods;
}

private Iterable<MethodSpec> generateBindMethods(GeneratedModelInfo classInfo) {
List<MethodSpec> methods = new ArrayList<>();

Expand Down Expand Up @@ -600,28 +667,6 @@ private Iterable<MethodSpec> generateStyleableViewMethods(GeneratedModelInfo mod
TypeName styleType = styleBuilderInfo.getTypeName();
ClassName styleBuilderClass = styleBuilderInfo.getStyleBuilderClass();

// buildView method to return new view instance
Builder buildViewMethodBuilder = MethodSpec.methodBuilder("buildView")
.addAnnotation(Override.class)
.addParameter(ClassNames.ANDROID_VIEW_GROUP, "parent")
.addModifiers(PROTECTED)
.returns(modelInfo.boundObjectTypeName)
.addStatement("$T v = new $T(parent.getContext())", modelInfo.boundObjectTypeName,
modelInfo.boundObjectTypeName)
.addStatement("v.setLayoutParams(parent.generateLayoutParams(null))");

addStyleApplierCode(buildViewMethodBuilder, styleBuilderInfo, "v");
buildViewMethodBuilder.addStatement("return v");
methods.add(buildViewMethodBuilder.build());

// getViewType method so that view type is generated at runtime
methods.add(MethodSpec.methodBuilder("getViewType")
.addAnnotation(Override.class)
.addModifiers(PROTECTED)
.returns(TypeName.INT)
.addStatement("return 0", modelInfo.boundObjectTypeName)
.build());

// setter for style object
Builder builder = MethodSpec.methodBuilder(PARIS_STYLE_ATTR_NAME)
.addModifiers(PUBLIC)
Expand Down Expand Up @@ -684,13 +729,14 @@ private Iterable<MethodSpec> generateMethodsReturningClassType(GeneratedModelInf
.varargs(methodInfo.varargs)
.returns(info.getParameterizedGeneratedName());

if (info.isStyleable()
if (info.isProgrammaticView()
&& "layout".equals(methodInfo.name)
&& methodInfo.params.size() == 1
&& methodInfo.params.get(0).type == TypeName.INT) {

builder
.addStatement("throw new $T(\"Layout resources are unsupported in @Styleable views.\")",
.addStatement(
"throw new $T(\"Layout resources are unsupported with programmatic views.\")",
UnsupportedOperationException.class);
} else {

Expand All @@ -716,10 +762,12 @@ private Iterable<MethodSpec> generateMethodsReturningClassType(GeneratedModelInf
private Iterable<MethodSpec> generateDefaultMethodImplementations(GeneratedModelInfo info) {
List<MethodSpec> methods = new ArrayList<>();

if (info.isStyleable()) {
if (info.isProgrammaticView()) {
methods.add(buildDefaultLayoutMethodBase()
.toBuilder()
.addStatement("throw new $T(\"Layout resources are unsupported in @Styleable views\")",
.addStatement(
"throw new $T(\"Layout resources are unsupported for views created programmatically"
+ ".\")",
UnsupportedOperationException.class)
.build());
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class ModelViewInfo extends GeneratedModelInfo {
boundObjectTypeName = ClassName.get(viewElement.asType());

saveViewState = viewAnnotation.saveViewState();
layoutParams = viewAnnotation.autoLayout();
fullSpanSize = viewAnnotation.fullSpan();
includeOtherLayoutOptions = configManager.includeAlternateLayoutsForViews(viewElement);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,4 +700,14 @@ public void callbackPropMustBeNullable() {
public void testModelBuilderInterface() {
assertGeneration("TestManyTypesView.java", "TestManyTypesViewModelBuilder.java");
}

@Test
public void testAutoLayout() {
assertGeneration("AutoLayoutModelView.java", "AutoLayoutModelViewModel_.java");
}

@Test
public void testAutoLayoutMatchParent() {
assertGeneration("AutoLayoutModelViewMatchParent.java", "AutoLayoutModelViewMatchParentModel_.java");
}
}
17 changes: 17 additions & 0 deletions epoxy-processortest/src/test/resources/AutoLayoutModelView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.airbnb.epoxy;

import android.content.Context;
import android.view.View;

@ModelView(autoLayout = ModelView.Size.WRAP_WIDTH_WRAP_HEIGHT)
public class AutoLayoutModelView extends View {

public AutoLayoutModelView(Context context) {
super(context);
}

@ModelProp
void setValue(int value) {

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

import android.content.Context;
import android.view.View;

import com.airbnb.epoxy.ModelView.Size;

@ModelView(autoLayout = Size.MATCH_WIDTH_MATCH_HEIGHT)
public class AutoLayoutModelViewMatchParent extends View {

public AutoLayoutModelViewMatchParent(Context context) {
super(context);
}

@ModelProp
void setValue(int value) {

}
}
Loading