Building reusable custom views - Android Community Forums
Register Members List Social Groups Calendar Search Today's Posts Mark Forums Read


Go Back   Android Community Forums > Blogs > divestoclimb


Rate this Entry

Building reusable custom views

Submit "Building reusable custom views" to Digg Submit "Building reusable custom views" to del.icio.us Submit "Building reusable custom views" to StumbleUpon Submit "Building reusable custom views" to Google
Posted 10-11-2009 at 06:21 PM by divestoclimb
Updated 10-11-2009 at 10:29 PM by divestoclimb

It was waaay too hard to figure all of this out, and I documented some of the solution on anddev.org, but it's pretty complex so I thought I'd lay it out step-by-step for how I built a real Spinner widget in Android.

I wanted to build a special widget class that would show an EditText and two buttons for entering numeric values. The two buttons would be plus and minus buttons, allowing the user to adjust the value in the box by a set increment. The box itself needed to have enforced upper and lower limits, and a precision. I also wanted to build the layout in XML.

The Android developer website has a section on Building Custom Components, but I found it pretty vague and didn't tell the whole story.

First, here's my layout for the widget, res/layout/num_selector.xml:
Code:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <ImageButton
        android:src="@drawable/minus"
        android:id="@+id/minus"
        android:layout_width="40sp"
        android:layout_height="40sp"/>
    <EditText
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="40sp"
        android:gravity="center"
        android:singleLine="true"/>
    <ImageButton
        android:src="@drawable/plus"
        android:id="@+id/plus"
        android:layout_width="40sp"
        android:layout_height="40sp"/>
</merge>
The <merge> tag means the contents below the root will get inserted into the parent ViewGroup directly. You will see in a moment that my class is going to extend a layout and inflate this XML into it; if I used a Layout here instead of <merge> it would still work, but I'd end up creating two levels of layout. This way I only create one additional level, which is more efficient.

This widget is going to be inserted into a layout one or more times in order for it to be useful. Here's how it will look in a layout file:
Code:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
       xmlns:app="http://schemas.android.com/apk/res/divestoclimb.gasmixer">
    <divestoclimb.gasmixer.NumberSelector
        android:id="@+id/number_o2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_below="@id/slider_o2"
        android:layout_centerHorizontal="true"
        app:textboxwidth="70sp"
        app:decimalplaces="1"
        app:lowerlimit="5"
        app:upperlimit="100"
        app:increment="1" />
[...]
Most of this is standard Android layout stuff, but all those "app:" attributes are my custom attributes for this widget. In order to get this to work, you have to specify the "xmlns:app=..." line in the root element. It doesn't have to point to a valid URL; when the compiler processes this line, it takes the last part of the URL, "divestoclimb.gasmixer" in this case, and uses that path to figure out what attributes are allowed under the "app" namespace. The rest can be made up.

Now you need to define the attributes in res/values/attrs.xml like so:
Code:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="NumberSelector">
        <attr name="textboxwidth" format="dimension" />
        <attr name="decimalplaces" format="integer" />
        <attr name="increment" format="float" />
        <attr name="lowerlimit" format="float" />
        <attr name="upperlimit" format="float" />
    </declare-styleable>
</resources>
Okay, that's all the XML we need. Now for the Java code: NumberSelector.java I'll only call attention to the high-level pieces that make it all work:

The class declaration
Code:
public class NumberSelector extends LinearLayout implements ... {
The class extends LinearLayout, which is why we can include it in a XML file. Everything in the <merge> section in our layout XML file is going to get added to this layout, so use the layout manager you want based on the widget design. My Trimix selector uses a RelativeLayout instead, for instance.

The constructors
Code:
    public NumberSelector(Context context) {
        super(context);
        initNumberSelector(context);
    }

    public NumberSelector(Context context, AttributeSet attrs) {
        super(context, attrs);
        initNumberSelector(context);
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.NumberSelector);
        
        int textWidth = a.getDimensionPixelSize(R.styleable.NumberSelector_textboxwidth, 0);
        if(textWidth > 0) {
            mEditText.setWidth(textWidth);
        }
        
        mIncrement = a.getFloat(R.styleable.NumberSelector_increment, 1);
        mLowerLimit = a.getFloat(R.styleable.NumberSelector_lowerlimit, 0);
        if(a.hasValue(R.styleable.NumberSelector_upperlimit)) {
            mUpperLimit = a.getFloat(R.styleable.NumberSelector_upperlimit, 0);
        } else {
            mUpperLimit = null;
        }
        setDecimalPlaces(a.getInt(R.styleable.NumberSelector_decimalplaces, 0));
        
        a.recycle();
    }
As far as I can tell, both constructors should be defined. To eliminate copy-pasting, I created a common method initNumberSelector() that does all the setup for both constructors that I'll get to in a minute. For now, just assume all it does is inflate our view from the XML, set up the member variables, and attach the internal class listeners like this:
Code:
    protected void initNumberSelector(Context context) {
        LayoutInflater i = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        i.inflate(SELECTOR_LAYOUT, this);

        mPlusButton = (ImageButton)findViewById(R.id.plus);
        mMinusButton = (ImageButton)findViewById(R.id.minus);
        mEditText = (EditText)findViewById(R.id.text1);
        if(mPlusButton != null) {
            mPlusButton.setOnClickListener(this);
        }
        if(mMinusButton != null) {
            mMinusButton.setOnClickListener(this);
        }
        mEditText.addTextChangedListener(this);
    }
The second constructor parses our attributes. It's mostly straightforward, but notice how I did "textboxwidth"; it's a dimension that can be specified in XML using any of the predefined Android screen units like "sp" or "dp". At the Java level, though, Android converts this to a pixel value for you using the getDimensionPixelSize() method, and with that I was able to set the EditText width in pixels using setWidth().

Public API
The following are the public methods for my widget, which will help in understanding how the rest works:
Code:
    /**
     * Set the value of the text field
     * @param value The value to set
     */
    public void setValue(float value);

    /**
     * Get the text field's current value
     * @return The current value of the field, or null if the value is invalid for the
     * current constraints
     */
    public Float getValue();

    public void setValueChangedListener(ValueChangedListener l);

    /**
     * Set the amount to increment the value by when the plus or minus button is pressed
     * @param increment The amount to increment
     */
    public void setIncrement(float increment);

    /**
     * Set the number of decimal places the text field allows.
     * @param places The number of places to allow.
     */
    public void setDecimalPlaces(int places);

    /**
     * Set the upper and lower limit for values in the text field.
     * @param lower_limit The lower limit (or null for no limit)
     * @param upper_limit The upper limit (or null for no limit)
     */
    public void setLimits(Float lower_limit, Float upper_limit);
setValue() and getValue() are the most important. They are the interface through which values can be changed programatically. I also allow attaching a custom listener to the widget so my activities can know when the value changes:
Code:
    public static interface ValueChangedListener {
        /**
         * Called when the value of the number in the NumberSelector changes
         * @param ns The NumberSelector containing the number that changed 
         * @param new_val The new value
         * @param from_user Set to true if the user manipulated the value manually.
         */
        abstract void onChange(NumberSelector ns, Float new_val, boolean from_user);
    }
Making that from_user piece work according to spec is a real bear which we'll see when I get into the finer details of making this work in my next post.
Posted in Uncategorized
Views 551 Comments 5 Email Blog Entry
« Prev     Main     Next »
Total Comments 5

Comments

  1. Old Comment
    Thanks! a lot, U r a saviour...

    I always wanted to do some thing similar, But unaware of <merge> tab, And Android Tut is too complex

    Is there any doc where we have reference to all this kind of tabs?

    Great Post... Very helpful
    Posted 10-12-2009 at 04:01 AM by Kumar Kumar is offline
  2. Old Comment
    Kumar,

    I linked to all the different sites where I found my original info. There are three bog posts by Romain Guy on the Android developer site on optimizing layouts, they go into two custom tags <include> and <merge> that I haven't seen in the official documentation.

    Glad you found it helpful! I have lots more I've been learning over the last few months that I'll explain in later posts.
    Posted 10-12-2009 at 05:18 PM by divestoclimb divestoclimb is offline
  3. Old Comment
    Very good post but can you explain how did the content of "num_selector.xml" get linked to your main layout file by the inclusion of "<divestoclimb.gasmixer.NumberSelector...>". I don't see the connection between "NumberSelector" and "num_selector.xml"...

    TKS
    Posted 10-13-2009 at 12:06 AM by TheKnowledgeSeeker TheKnowledgeSeeker is offline
    Updated 10-13-2009 at 12:09 AM by TheKnowledgeSeeker (Hit the enter button before completing.)
  4. Old Comment
    Sorry, I didn't mention that I defined the constant SELECTOR_LAYOUT to be R.layout.num_selector. The contents of the layout are inflated within the class when it's instantiated.

    For some reason ADT barfs on that layout in the UI designer throwing a ClassCastException, but it works fine running in Android.
    Posted 10-13-2009 at 11:08 PM by divestoclimb divestoclimb is offline
  5. Old Comment
    divestoclimb,

    That did the trick. Thanks for the help.

    TKS
    Posted 10-14-2009 at 04:59 PM by TheKnowledgeSeeker TheKnowledgeSeeker is offline
 

All times are GMT -5. The time now is 10:20 PM.



Copyright © 2008-2009 Android Community / R3 Media LLC, All Rights Reserved.