131

I created a custom View (find it here) with an declare-styleable attribute of type enum. In xml I can now choose one of the enum entries for my custom attribute. Now I want to create an method to set this value programmatically, but I can not access the enum.

attr.xml

<declare-styleable name="IconView">
    <attr name="icon" format="enum">
        <enum name="enum_name_one" value="0"/>
        ....
        <enum name="enum_name_n" value="666"/>
   </attr>
</declare-styleable>     

layout.xml

<com.xyz.views.IconView
    android:id="@+id/heart_icon"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:icon="enum_name_x"/>

What I need is something like: mCustomView.setIcon(R.id.enum_name_x); But I can not find the enum or I even have no idea how I can get the enum or the names of the enum.

Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
Informatic0re
  • 6,300
  • 5
  • 41
  • 56

6 Answers6

109

There does not seem to be an automated way to get a Java enum from an attribute enum - in Java you can get the numeric value you specified - the string is for use in XML files (as you show).

You could do this in your view constructor:

TypedArray a = context.getTheme().obtainStyledAttributes(
                attrs,
                R.styleable.IconView,
                0, 0);

    // Gets you the 'value' number - 0 or 666 in your example
    if (a.hasValue(R.styleable.IconView_icon)) {
        int value = a.getInt(R.styleable.IconView_icon, 0));
    }

    a.recycle();
}

If you want the value into an enum you would need to either map the value into a Java enum yourself, e.g.:

private enum Format {
    enum_name_one(0), enum_name_n(666);
    int id;

    Format(int id) {
        this.id = id;
    }

    static Format fromId(int id) {
        for (Format f : values()) {
            if (f.id == id) return f;
        }
        throw new IllegalArgumentException();
    }
}

Then in the first code block you could use:

Format format = Format.fromId(a.getInt(R.styleable.IconView_icon, 0))); 

(though throwing an exception at this point may not be a great idea, probably better to choose a sensible default value)

mykolaj
  • 974
  • 8
  • 17
Andy Mell
  • 1,091
  • 1
  • 6
  • 3
  • 1
    This is the cleanest way of doing it. But it could be much better if it was possible to have the enum to id mapping at one place. With this way, we have to define the mapping both in xml attributes and in enum class. – Tushar Kathuria Sep 02 '20 at 05:57
58

It's simple let's show everybody an example just to show how easy it is:

attr.xml:

<declare-styleable name="MyMotionLayout">
    <attr name="motionOrientation" format="enum">
        <enum name="RIGHT_TO_LEFT" value="0"/>
        <enum name="LEFT_TO_RIGHT" value="1"/>
        <enum name="TOP_TO_BOTTOM" value="2"/>
        <enum name="BOTTOM_TO_TOP" value="3"/>
    </attr>
</declare-styleable>

Custom layout:

public enum Direction {RIGHT_TO_LEFT, LEFT_TO_RIGHT, TOP_TO_BOTTOM, BOTTOM_TO_TOP}
Direction direction;
...
    TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.MyMotionLayout);
    Direction direction = Direction.values()[ta.getInt(R.styleable.MyMotionLayout_motionOrientation,0)];

now use direction like any other enumeration variable.

Malwinder Singh
  • 6,644
  • 14
  • 65
  • 103
Steve Moretz
  • 2,758
  • 1
  • 17
  • 31
  • In conclusion, just use this to get Enum attr: TypedArray.getInt(R.styleable.name_your_define, defaultValue) – CalvinChe May 08 '19 at 06:47
  • @CalvinChe ,, that returns an `int`. Steve Moretz has it. I feel dumb for not seeing it, _but it is 4:30AM_. Time for bed... – n00dles May 09 '19 at 04:37
  • Yeah just as simple as that.The answer was just an example for illustrating it better. – Steve Moretz May 09 '19 at 09:54
  • 9
    But this just makes two parallel definitions of symbols. This works only so long as the definitions are identical - that is, it is fragile. The OP was expecting code access to an enum generated from the XML. It seems like that should be possible. – Steve White Jun 11 '20 at 14:55
  • @SteveWhite since you have no access to xml then there is no way to automate it and let it depend on one definition.This is the cleanest (possible) way to achieve it.If you could parse the xml and access it in a way that could be great but you can't.(Unless you can but not in the code,you can write a plugin to read the xml and extract the values to your java so it will be syncronized so yeah that way it won't be fragile because it's automated.) – Steve Moretz Jun 11 '20 at 19:05
23

Let me add a solution written in kotlin. Add inline extension function:

inline fun <reified T : Enum<T>> TypedArray.getEnum(index: Int, default: T) =
    getInt(index, -1).let { if (it >= 0) enumValues<T>()[it] else default 
}

Now getting enum is simple:

val a: TypedArray = obtainStyledAttributes(...)
val yourEnum: YourEnum = a.getEnum(R.styleable.YourView_someAttr, YourEnum.DEFAULT)
a.recycle()
Joshua King
  • 3,450
  • 1
  • 17
  • 19
Oleksandr Albul
  • 1,611
  • 1
  • 23
  • 31
13

Well for sanity's sake. Make sure your ordinals are the same in your declared styleable as in your Enum declaration and access it as an array.

TypedArray a = context.getTheme().obtainStyledAttributes(
                   attrs,
                   R.styleable.IconView,
                   0, 0);

int ordinal = a.getInt(R.styleable.IconView_icon, 0);

if (ordinal >= 0 && ordinal < MyEnum.values().length) {
      enumValue = MyEnum.values()[ordinal];
}
Harald K
  • 26,314
  • 7
  • 65
  • 111
Dave Thomas
  • 3,667
  • 2
  • 33
  • 41
  • 3
    I think relying on enum ordinals here is destined to create unreliable code. One thing will get updated and not the other and then you'll have trouble. – tir38 Nov 20 '17 at 20:23
  • 2
    so what is a better way of doing it? – Jono Aug 13 '18 at 09:15
5

I know it's been a while since the question was posted, but I had the same issue recently. I hacked a little something together that uses Square's JavaPoet and some stuff in the build.gradle that automatically creates a Java enum class from the attrs.xml on project build.

There's a little demo and a readme with an explanation at https://github.com/afterecho/create_enum_from_xml

Hope it helps.

Darren
  • 197
  • 1
  • 4
0

What I did i kotlin, was getting them with usage of companion object factory, like so. Here an exmaple of simple sorting view.

  1. Define your enum in xml

     <declare-styleable name="SortingView">
     <attr name="sortOrder" format="enum">
         <enum name="ASC" value="0"/>
         <enum name="DESC" value="1" />
     </attr>
    
  2. Define enum in code, add companion method to return proper enum value depending on it's id.

     enum class SortOrder(val id:Int) {
     ASC(0),
     DESC(1);
    
     companion object {
         fun fromParams(id:Int):SortOrder {
             return when(id) {
                 0 -> ASC
                 1 -> DESC
                 else -> throw IllegalAccessException("Unsupported SortOrder")
             }
         }
     }
    

    }

  3. Get The enum values from typed array with getInt() method, and pass it to the factory to create your enum.

     sortOrder = SortOrder.fromParams(getInt(R.styleable.SortingView_sortOrder, SortOrder.ASC.id))
    

Now you can use your enum in xml like so enter image description here and also use your enum class in the code like in step 3.

The only downside is, they had to be defined in code and in xml as well, but works like a charm.