Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a custom XML Data type?

Tags:

android

xml

Is there a way to create a custom XML data type for Android?

I have a class Model that contains all of the statistics of my entities. I want to be able to inflate Model class from xml similar - well, exaclty as View's do. Is this possible?

Example:

<?xml version="1.0" encoding="utf-8"?>
<models xmlns:android="http://schemas.android.com/apk/res/android">
    <model name="tall_model"
        type="@string/infantry"
        stat_attack="5"
        >Tall Gunner</model>

    <model name="short_model"
        type="@string/infantry"
        stat_attack="3"
        ability="@resource/scout"
        >Short Gunner</model>

    <model name="big_tank"
        type="@string/vehicle"
        stat_attack="7"
        armour="5"
        >Big Tank</model>
</models>

And the class I would like to inflate.

class Model [extends Object] {
    public Model(Context context, AttributeSet attrs) {
        // I think you understand what happens here.
    }
    // ...
}
like image 787
ahodder Avatar asked Dec 04 '11 21:12

ahodder


1 Answers

With some custom code using carefully selected APIs, you can mimic the way Android inflates layout XML files and still benefit from the XML optimizations and goodies that Android has like compiled XML files and references to arbitrary resources within your custom XML files. You can't directly hook in into the existing LayoutInflater because that class can only deal with inflating Views. In order for the below code to work, put your XML file in 'res/xml' in your application.

First, here's the code that parses the (compiled) XML file and invokes the Model constructor. You might want to add some registering mechanism so that you can easily register a class for any tag, or you might want to use ClassLoader.loadClass() so that you can load classes based on their name.

public class CustomInflator {
    public static ArrayList<Model> inflate(Context context, int xmlFileResId) throws Exception {
        ArrayList<Model> models = new ArrayList<Model>();

        XmlResourceParser parser = context.getResources().getXml(R.xml.models);
        Model currentModel = null;
        int token;
        while ((token = parser.next()) != XmlPullParser.END_DOCUMENT) {
            if (token == XmlPullParser.START_TAG) {
                if ("model".equals(parser.getName())) {
                    // You can retrieve the class in other ways if you wish
                    Class<?> clazz = Model.class;
                    Class<?>[] params = new Class[] { Context.class, AttributeSet.class };
                    Constructor<?> constructor = clazz.getConstructor(params);
                    currentModel = (Model)constructor.newInstance(context, parser);
                    models.add(currentModel);
                }
            } else if (token == XmlPullParser.TEXT) {
                if (currentModel != null) {
                    currentModel.setText(parser.getText());
                }
            } else if (token == XmlPullParser.END_TAG) {
                // FIXME: Handle when "model" is a child of "model"
                if ("model".equals(parser.getName())) {
                    currentModel = null;
                }
            }
        }

        return models;
    }
 }

With this in place, you can put the "parsing" of the attributes inside the Model class, much like View does it:

public class Model {
    private String mName;
    private String mType;
    private int mStatAttack;
    private String mText;

    public Model(Context context, AttributeSet attrs) {
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            String attr = attrs.getAttributeName(i);
            if ("name".equals(attr)) {
                mName = attrs.getAttributeValue(i);
            } else if ("type".equals(attr)) {
                // This will load the value of the string resource you
                // referenced in your XML
                int stringResource = attrs.getAttributeResourceValue(i, 0);
                mType = context.getString(stringResource);
            } else if ("stat_attack".equals(attr)) {
                mStatAttack = attrs.getAttributeIntValue(i, -1);
            } else {
                // TODO: Parse more attributes
            }
        }
    }

    public void setText(String text) {
        mText = text;
    }

    @Override
    public String toString() {
        return "model name=" + mName + " type=" + mType + " stat_attack=" + mStatAttack + " text=" + mText;
    }
}

Above I have referenced attributes through the string representation. If you want to go further, you can define application specific attribute resources and use those instead, but that will complicate things quite a bit (see Declaring a custom android UI element using XML). Anyway, with all resources setup and this in a dummy activity:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    try {
        for (Model m : CustomInflator.inflate(this, R.xml.models)) {
            Log.i("Example", "Parsed: " + m.toString());
        }
    } catch (Exception e) {
        Log.e("Example", "Got " + e);
    }
}

you'll get this output:

I/Example ( 1567): Parsed: model name=tall_model type=Example3 stat_attack=5 text=Tall Gunner
I/Example ( 1567): Parsed: model name=short_model type=Example3 stat_attack=3 text=Short Gunner
I/Example ( 1567): Parsed: model name=big_tank type=Example2 stat_attack=7 text=Big Tank

Note that you can't have @resource/scout in your XML file since resource is not a valid resource type, but @string/foo works fine. You should also be able to use for example @drawable/foo with some trivial modifications to the code.

like image 167
Martin Nordholts Avatar answered Sep 22 '22 07:09

Martin Nordholts