Skip to content

Custom View Attributes

Nathanael Silverman edited this page Oct 24, 2018 · 3 revisions

In addition to supporting the application of custom attributes, Paris helps get rid of the boilerplate associated with adding custom attributes to your views in the first place.

Custom attributes are declared in XML as you normally would:

<declare-styleable name="MyView">
    <attr name="title" format="string" />
    <attr name="image" format="reference" />
    <attr name="imageSize" format="dimension" />
</declare-styleable>

Next we'll use two annotations to give our custom view information about its attributes.

  • @Styleable is a class annotation that references the name of our styleable declaration
  • @Attr is a method annotation that references a particular styleable attribute

When Paris is used to apply either an AttributeSet or a style resource to our custom view, the annotated methods will automatically be called with the corresponding attribute value.

Here's an example:

// The value here corresponds to the name chosen in declare-styleable.
@Styleable("MyView")
class MyView(…) : ViewGroup(…) {

    init {
        // This call enables the custom attributes when used in XML layouts. It
        // extracts styling information from AttributeSet like it would a StyleRes.
        style(attrs)
    }

    @Attr(R.styleable.MyView_title)
    fun setTitle(title: String) {
        // Automatically called with the title value (if any) when an AttributeSet
        // or StyleRes is applied to the MyView instance.
    }

    @Attr(R.styleable.MyView_image)
    fun setImage(image: Drawable?) {
        // Automatically called with the image value (if any) when an AttributeSet
        // or StyleRes is applied to the MyView instance.
    }

    @Attr(R.styleable.MyView_imageSize)
    fun setImageSize(@Px imageSize: Int) {
        // Automatically called with the imageSize value (if any) when an
        // AttributeSet or StyleRes is applied to the MyView instance.
    }
}
Click to see the example in Java.
// The value here corresponds to the name chosen in declare-styleable.
@Styleable("MyView")
public class MyView extends ViewGroup {

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

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyle) {
        this(context, attrs, defStyle);
        // This call enables the custom attributes when used in XML layouts. It
        // extracts styling information from AttributeSet like it would a StyleRes.
        Paris.style(this).apply(attrs);
    }

    @Attr(R.styleable.MyView_title)
    public void setTitle(String title) {
        // Automatically called with the title value (if any) when an AttributeSet
        // or StyleRes is applied to the MyView instance.
    }

    @Attr(R.styleable.MyView_image)
    public void setImage(Drawable image) {
        // Automatically called with the image value (if any) when an AttributeSet
        // or StyleRes is applied to the MyView instance.
    }

    @Attr(R.styleable.MyView_imageSize)
    public void setImageSize(@Px int imageSize) {
        // Automatically called with the imageSize value (if any) when an
        // AttributeSet or StyleRes is applied to the MyView instance.
    }
}

In Kotlin, the annotated functions must be public (default) or internal. In Java, the annotated methods must be public or package-private.

That's it!

We can now use these custom attributes in an XML layout:

<MyViewapp:title="@string/hello"
    app:image="@drawable/beach"
    app:imageSize="@dimen/huge" />

Or in an XML style:

<style name="Beach">
    <item name="title">@string/hello</item>
    <item name="image">@drawable/beach</item>
    <item name="imageSize">@dimen/huge</item>
    <item name="android:textColor">#FFFF00</item>
</style>

Which can then be applied in XML:

<MyView
    style="@style/Beach"
    … />

Or programmatically:

myView.style(R.style.Beach)
Click to see the example in Java.
Paris.style(myView).apply(R.style.Beach);

We also have access to these new attributes via the style builders:

myView.style {
    title("Hello") // Using an actual value.
    image(R.drawable.beach) // Or a resource.
    …
}
Click to see the example in Java.
Paris.styleBuilder(myView)
        .title("Hello") // Using an actual value.
        .image(R.drawable.beach) // Or a resource.
        …
        .apply();

Attention: Extension functions like title and image are generated during compilation by the Paris annotation processor. When new @Attr annotations are added, the project must be (re)compiled once for the related functions to become available.

Attribute Formats and Parameter Types

Paris converts the attribute value based on the type of the method parameter, as well as the use of @Fraction and support annotations like @ColorInt, @ColorRes, @DimenRes, all the other @…Res, and @Px.

The following conversion table indicates which method parameter types can be used depending on the styleable attribute format (as declared in <attr … format="…" />).

Attribute Format Parameter Type
boolean boolean
color @ColorInt int
dimension @LayoutDimension int
dimension @Px int
enum int
flag int
float float
fraction @Fraction(base = ?, pbase = ?) float
integer int
reference @AnyRes* int
reference CharSequence[]
reference ColorStateList
reference Drawable
reference Typeface
string CharSequence
string String
string Typeface

* Any of the @…Res annotations can be used

Paris doesn't know how to parse attribute values properly unless the method parameters are properly annotated. For example, an attribute declared with format="dimension" used on a method whose parameter is not annotated with @Px will be parsed as a normal integer, resulting in an error.