Building reusable custom views
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:
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:
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:
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
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
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:
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:
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:
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.
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>
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" />
[...]
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>
The class declaration
Code:
public class NumberSelector extends LinearLayout implements ... {
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();
}
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);
}
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);
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);
}
Total Comments 5
Comments
-
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 helpfulPosted 10-12-2009 at 04:01 AM by Kumar
-
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
-
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"...
TKSPosted 10-13-2009 at 12:06 AM by TheKnowledgeSeeker
Updated 10-13-2009 at 12:09 AM by TheKnowledgeSeeker (Hit the enter button before completing.) -
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,
That did the trick. Thanks for the help.
TKSPosted 10-14-2009 at 04:59 PM by TheKnowledgeSeeker







