One can easily find documentation for that at http://developer.android.com/guide/topics/ui/custom-components.html. However, it is not clear how to pass parameters to the newly-created custom component via Layout XML this article will teach you how to do so.
To create a compound component, follow the steps below:
- Create the layout XML putting in the /res/layout directory as you would do with any layout XML file for UI. This file holds the compound UI of the component to be created.
- Create a class which extends a layout. This class will inflate holding the component XML file dscribed in 1.. In this class, as mentioned, inflate the layout and setup the component as if it were an ordinary UI.
- If one wishes to add attributes to be set in the layout XML file which uses the component, follow the steps below:
- In /res/values create a XML file called attrs.xml. The file could have any name one wishes. Within this file, add component with a declare-stylable tag and within it, add the attributes to be set in the component when it is used in the UI. An example will be provided later.
- In the onCreate event of the layout class of the component, get each attribute described above and save it in the component. These attributes will hold the values set in the layout XML, passed to the created component.
- Add the component to the UI as you would do with any other Android Element.
Now, examples will be displayed for the steps depcited above. Firstly the component layout XML is created:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="3dip">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dip"
android:orientation="horizontal">
<TextView
android:id="@+id/txvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_component_element_name"
android:textAppearance="?android:attr/textAppearanceLarge" />
<TextView
android:id="@+id/txvstatusMessage"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dip"
android:textStyle="italic"
android:textSize="12sp"
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dip"
android:orientation="horizontal">
<AutoCompleteTextView
android:id="@+id/atcElements"
android:layout_width="256dp"
android:layout_height="wrap_content"
android:layout_weight="0.65"
android:ems="28"
android:inputType="text"
android:scrollHorizontally="true" >
</AutoCompleteTextView>
<ImageButton
android:id="@+id/btnPick"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:src="@drawable/ic_search"
android:contentDescription="@string/label_component_pick_up_button_description_message"
android:background="@android:color/background_dark"
android:text="@string/button_select" />
</LinearLayout>
</LinearLayout>
|
Note that the XML layout which defines the component is an ordinary layout. It holds imagebuttons, autocomplete elements, layouts and text view elements.
This componet is basically a text box with a search button. One adds a list of elements to this component and when the search button is clicked, all elements are displayed. Moreover, when the user types a name in the text box, a list of possible matches are displayed. Finally, message of new elements are displayed in case the entered text does not belong to thie list. These messages and the labels displayed are entered in the UI which uses this component.
Below is displayed the full code of the component´s activity. The most important parts of it will be discussed later.
Below is displayed the full code of the component´s activity. The most important parts of it will be discussed later.
package
com.example.componentsample.component;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import
android.content.res.TypedArray;
import android.text.Editable;
import android.text.TextWatcher;
import
android.util.AttributeSet;
import android.util.TypedValue;
import
android.view.LayoutInflater;
import android.view.View;
import
android.widget.ArrayAdapter;
import
android.widget.AutoCompleteTextView;
import
android.widget.ImageButton;
import
android.widget.LinearLayout;
import android.widget.TextView;
import
com.example.componentsample.R;
import
com.example.componentsample.listener.DialogCancelClickListener;
import
com.example.componentsample.message.Standards;
import
com.example.componentsample.utils.ActivityUtils;
/**
* This component holds an
entity to be set or retrieved. This component,
* however have more
functionalities such as auto complete for a list of these
* entities and a selection of
these as well, in a list. Moreover, message for
* the existence of the written
element are displayed as one types in the text
* area.
*
* @author Eduardo
*
*/
public class
ElementPickerComponent extends LinearLayout {
private TextView txvName = null;
private AutoCompleteTextView atcElements = null;
private ImageButton btnPick = null;
private TextView txvStatusMessage = null;
private String[] elementList = null;
private CharSequence messageNoElement = null;
private CharSequence messagePickUpElement = null;
private CharSequence messageNewElement = null;
private ElementPickerTextChanged textChangedEvent = null;
public static final String
TEXT_WIDTH__SMALL_KEY = "small";
public static final String
TEXT_WIDTH__MEDIUM_KEY = "medium";
public static final int TEXT_WIDTH__SMALL_VALUE = 100;
public static final int TEXT_WIDTH__MEDIUM_VALUE = 160;
public
ElementPickerComponent(Context context, AttributeSet attrs) {
super(context,
attrs);
// set up layout
LayoutInflater
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.component__element_picker, this);
setLayoutParams(new
LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
// assign components
txvName =
(TextView) findViewById(R.id.txvName);
txvStatusMessage = (TextView) findViewById(R.id.txvstatusMessage);
atcElements =
(AutoCompleteTextView) findViewById(R.id.atcElements);
btnPick =
(ImageButton) findViewById(R.id.btnPick);
// add listeners
btnPick.setOnClickListener(new
PickUpClickListener());
atcElements.addTextChangedListener(new
ElementChangedTextWatcher());
// handle custom properties
handleCustomProperties(attrs);
// in case there are elements, bind it to autocomplete
setAutoCompleteList(true);
}
/**
* Attach the autocomplete list to the
element.
*
* @param callOnTextChangedEvent
*
In case one wishes to call
the
*
{@link ElementPickerTextChanged} event, while setting the
*
element list, set <code>true</code> to this variable,
*
otherwise set <code>false</code>.
*/
private void
setAutoCompleteList(boolean callOnTextChangedEvent) {
if (elementList != null) {
ArrayAdapter<String>
arrayAdapter = new ArrayAdapter<String>(this.getContext(),
android.R.layout.simple_dropdown_item_1line, elementList);
atcElements.setAdapter(arrayAdapter);
}
// update the element text status since the element
list is redefined
boolean
isFound = updateElementTextStatus();
// call the text changed event, in case there is any
if (textChangedEvent != null) {
textChangedEvent.onTextChanged(atcElements.getText().toString(), isFound);
}
}
/**
* Get custom properties and set them
accordingly.
*
* @param attrs
*
{@link AttributeSet} holding the custom attributes.
*/
private void
handleCustomProperties(AttributeSet attrs) {
// get custom attributes
TypedArray
componentAttributes = getContext().obtainStyledAttributes(attrs, R.styleable.ElementPickerComponent);
CharSequence text =
componentAttributes.getText(R.styleable.ElementPickerComponent_nameText);
// set the name of the field
if (text != null) {
txvName.setText(text);
}
// set content description of the pick up button
text =
componentAttributes.getText(R.styleable.ElementPickerComponent_pickupButtonContentDescription);
if (text != null) {
btnPick.setContentDescription(text);
}
// set text width
text =
componentAttributes.getText(R.styleable.ElementPickerComponent_textWidth);
if (text != null) {
float
widthPixels = 0f;
if (text.equals(TEXT_WIDTH__SMALL_KEY)) {
widthPixels
= TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, TEXT_WIDTH__SMALL_VALUE, getResources().getDisplayMetrics());
atcElements.setLayoutParams(new
LayoutParams((int) widthPixels, LayoutParams.WRAP_CONTENT));
} else if (text.equals(TEXT_WIDTH__MEDIUM_KEY)) {
widthPixels
= TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, TEXT_WIDTH__MEDIUM_VALUE, getResources().getDisplayMetrics());
atcElements.setLayoutParams(new
LayoutParams((int) widthPixels, LayoutParams.WRAP_CONTENT));
}
}
// set no elements message
text =
componentAttributes.getText(R.styleable.ElementPickerComponent_messageNoElements);
if (text != null) {
messageNoElement = text;
} else {
messageNoElement = getResources().getString(R.string.msg_no_elements);
}
// set pick up element message
text =
componentAttributes.getText(R.styleable.ElementPickerComponent_messagePickUpElement);
if (text != null) {
messagePickUpElement = text;
} else {
messagePickUpElement = getResources().getString(R.string.msg_pick_up_element);
}
// set new element message
text =
componentAttributes.getText(R.styleable.ElementPickerComponent_messageNewElement);
if (text != null) {
messageNewElement = text;
} else {
messageNewElement = getResources().getString(R.string.msg_element_is_new);
}
}
/**
* Update Element text status in order to
determined whether it is new or
* not.
*
* @return In case the element
text is new, <code>true</code> is returned,
*
otherwise <code>false</code> is given back.
*/
private boolean
updateElementTextStatus() {
boolean
isFound = false;
String elementText = atcElements.getText().toString().trim();
// when nothing is entered display no message
if
(elementText.equals(Standards.EMPTY_STRING)) {
isFound = true;
}
// check whether the current element text is in the
list of elements
else if (elementList != null
&& elementList.length > 0) {
for
(String elementInList : elementList) {
if
(elementText.equals(elementInList.trim())) {
// it is indeed
isFound
= true;
break;
}
}
}
// in case the element text is not in the list, show
the new element
// message
if (!isFound) {
txvStatusMessage.setText(messageNewElement);
} else {
txvStatusMessage.setText(Standards.EMPTY_STRING);
}
return
isFound;
}
/**
* Action performed when the element name is
changed. When every key is
* pressed this event will be called. Here a
message of a new element is
* displayed whenever the entered name cannot
be found in the element's
* list.
*
* @author Eduardo
*
*/
private class
ElementChangedTextWatcher implements TextWatcher {
public void
onTextChanged(CharSequence s, int start, int before, int count) {
// do nothing
}
public void
beforeTextChanged(CharSequence s, int start, int count,
int after) {
// do nothing
}
public void
afterTextChanged(Editable s) {
// update text status
boolean
isFound = updateElementTextStatus();
// call the text changed event, in case there is any
if (textChangedEvent != null) {
textChangedEvent.onTextChanged(atcElements.getText().toString(), isFound);
}
}
}
/**
* Action for when the pick up button is
selected. Here a list of elements
* are displayed for selections, or a message
stating there are no elements
* to be selected is shown.
*
* @author Eduardo
*
*/
private class
PickUpClickListener implements OnClickListener {
/**
* Action dispatched when the pick up button
was clicked.
*/
public void
onClick(View v) {
Dialog dialog =
null;
// show list of element to be picked
if (elementList != null
&& elementList.length > 0) {
dialog =
ActivityUtils.createListOptionsDialogBuilder(getContext(), messagePickUpElement.toString(), elementList,
new
DialogInterface.OnClickListener() {
public void
onClick(DialogInterface dialog, int which) {
// set selected element
atcElements.setText(elementList[which]);
}
});
} else {
// show message stating there is no element to be
selected
dialog =
ActivityUtils.createOKDialogBuilder(getContext(), messageNoElement.toString(), new DialogCancelClickListener());
}
dialog.show();
}
}
/**
* Add event which is dispatched after the
text has changed.
*
* @param event
*
Event to be added. As mentioned before, this event is launched
*
after the text has changed.
*/
public void
addTextChangedListener(ElementPickerTextChanged event) {
this.textChangedEvent = event;
}
/**
* Get the message stating the element is
new.
*
* @return The message stating the
element is new.
*/
public CharSequence
getMessageNewElement() {
return messageNewElement;
}
/**
* Set the message stating the element is
new.
*
* @param messageNewElement
*
The message stating the element is new, to be set.
*/
public void
setMessageNewElement(CharSequence messageNewElement) {
this.messageNewElement = messageNewElement;
}
/**
* Get the message for picking up an element.
*
* @return The message for picking
up an element.
*/
public CharSequence getMessagePickUpElement()
{
return messagePickUpElement;
}
/**
* Set the message for picking up an element.
*
* @param messagePickUpElement
*
The message for picking up an element.
*/
public void
setMessagePickUpElement(CharSequence messagePickUpElement) {
this.messagePickUpElement = messagePickUpElement;
}
/**
* Get the message which states that there is
no element when the pick up
* button is pressed.
*
* @return the message displayed
when the pick up button is pressed and
*
there is no element to be selected.
*/
public CharSequence
getMessageNoElement() {
return messageNoElement;
}
/**
* Set the message which states that there is
no element when the pick up
* button is pressed.
*
* @param noElementMessage
*
The message displayed when the pick up button is pressed and
*
there is no element to be selected.
*/
public void
setMessageNoElement(CharSequence noElementMessage) {
this.messageNoElement = noElementMessage;
}
/**
* Get the list of elements used for choice
in this component.
*
* @return List of elements used
for choice.
*/
public String[] getElementList()
{
return elementList;
}
/**
* Set the list of elements used for choice
in this component.
*
* @param elementList
*
List of elements used for choice.
*
* @param callOnTextChangedEvent
*
In case one wishes to call the
*
{@link ElementPickerTextChanged} event, while setting the
*
element list, set <code>true</code> to this variable,
*
otherwise set <code>false</code>.
*/
public void
setElementList(String[] elementList, boolean callOnTextChangedEvent) {
this.elementList =
elementList;
// attach list to the autocomplet component
setAutoCompleteList(callOnTextChangedEvent);
}
/**
* Get the content description for the pick
up button. This is useful for
* accessibility purposes.
*
* @return The Pick Up button
content description.
*/
public CharSequence
getPickUpButtonContentDescription() {
return btnPick.getContentDescription();
}
/**
* Set the Pick Up button content
description. This is useful for
* accessibility purposes.
*
* @param contentDescription
*
The content description to be set.
*/
public void
setPickUpButtonContentDescription(CharSequence contentDescription) {
btnPick.setContentDescription(contentDescription);
}
/**
* Get the Name which this component holds.
*
* @return The name this component
holds.
*/
public String getName() {
return txvName.getText().toString();
}
/**
* Set the Name which this component holds.
*
* @param name
*
Name to be set.
*/
public void
setName(String name) {
txvName.setText(name);
}
/**
* Get the text entered for this component.
*
* @return The text entered for
this component.
*/
public String getText() {
return atcElements.getText().toString();
}
/**
* Set the text for this component.
*
* @param text
*
Text to be set for this component.
*/
public void
setText(String text) {
atcElements.setText(text);
}
}
|
As one must have noticed this class is huge. However, it simply setups the componet as a normal UI.
Below is displayed where the setup is made. Note that the layout is inflated, components are initialized, listeners are set and the method handleCustomProperties(AttributeSet) is called - this method fetches parameters specified by the UIs which uses this very component.
The interesting stuff is displayed below, which is where the parameters are retrieved from the layout XML class which uses this component.
Below is displayed where the setup is made. Note that the layout is inflated, components are initialized, listeners are set and the method handleCustomProperties(AttributeSet) is called - this method fetches parameters specified by the UIs which uses this very component.
public ElementPickerComponent(Context context, AttributeSet attrs) {
super(context, attrs); // set up layout LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.component__element_picker, this); setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); // assign components txvName = (TextView) findViewById(R.id.txvName); txvStatusMessage = (TextView) findViewById(R.id.txvstatusMessage); atcElements = (AutoCompleteTextView) findViewById(R.id.atcElements); btnPick = (ImageButton) findViewById(R.id.btnPick); // add listeners btnPick.setOnClickListener(new PickUpClickListener()); atcElements.addTextChangedListener(new ElementChangedTextWatcher()); // handle custom properties handleCustomProperties(attrs); // in case there are elements, bind it to autocomplete setAutoCompleteList(true); } |
The interesting stuff is displayed below, which is where the parameters are retrieved from the layout XML class which uses this component.
/**
* Get custom properties and set them
accordingly.
*
* @param attrs
*
{@link AttributeSet} holding the custom attributes.
*/
private void
handleCustomProperties(AttributeSet attrs) {
// get custom attributes
TypedArray
componentAttributes = getContext().obtainStyledAttributes(attrs, R.styleable.ElementPickerComponent);
CharSequence text =
componentAttributes.getText(R.styleable.ElementPickerComponent_nameText);
// set the name of the field
if (text != null) {
txvName.setText(text);
}
// set content description of the pick up button
text =
componentAttributes.getText(R.styleable.ElementPickerComponent_pickupButtonContentDescription);
if (text != null) {
btnPick.setContentDescription(text);
}
// set text width
text =
componentAttributes.getText(R.styleable.ElementPickerComponent_textWidth);
if (text != null) {
float
widthPixels = 0f;
if (text.equals(TEXT_WIDTH__SMALL_KEY)) {
widthPixels
= TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, TEXT_WIDTH__SMALL_VALUE, getResources().getDisplayMetrics());
atcElements.setLayoutParams(new
LayoutParams((int) widthPixels, LayoutParams.WRAP_CONTENT));
} else if (text.equals(TEXT_WIDTH__MEDIUM_KEY)) {
widthPixels
= TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, TEXT_WIDTH__MEDIUM_VALUE, getResources().getDisplayMetrics());
atcElements.setLayoutParams(new
LayoutParams((int) widthPixels, LayoutParams.WRAP_CONTENT));
}
}
// set no elements message
text =
componentAttributes.getText(R.styleable.ElementPickerComponent_messageNoElements);
if (text != null) {
messageNoElement = text;
} else {
messageNoElement = getResources().getString(R.string.msg_no_elements);
}
// set pick up element message
text =
componentAttributes.getText(R.styleable.ElementPickerComponent_messagePickUpElement);
if (text != null) {
messagePickUpElement = text;
} else {
messagePickUpElement = getResources().getString(R.string.msg_pick_up_element);
}
// set new element message
text =
componentAttributes.getText(R.styleable.ElementPickerComponent_messageNewElement);
if (text != null) {
messageNewElement = text;
} else {
messageNewElement = getResources().getString(R.string.msg_element_is_new);
}
}
|
Here is where the "magic" happens. The attributes retrieved here were set in the UI which uses this component. To define these attributes one must create the attrs.xml,which is displayed below.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ElementPickerComponent">
<attr name="nameText" format="integer" />
<attr name="textWidth" format="string" />
<attr name="pickupButtonContentDescription" format="integer" />
<attr name="messageNoElements" format="integer" />
<attr name="messagePickUpElement" format="integer" />
<attr name="messageNewElement" format="integer" />
</declare-styleable>
</resources>
|
Note how the attributes are defined and the component is explicitly declared. Now, to wrap up, the layout XML which uses the component.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:cc="http://schemas.android.com/apk/res/com.example.componentsample"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
tools:context=".MainActivity"
>
<com.example.componentsample.component.ElementPickerComponent
android:id="@+id/namePickerComponent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
cc:nameText="@string/label_name"
cc:pickupButtonContentDescription="@string/msg_accessibility_select_product"
cc:messageNoElements="@string/msg_no_products_registered"
cc:messagePickUpElement="@string/msg_pick_up_product"
cc:messageNewElement="@string/msg_product_is_new"
android:background="@android:color/black"/>
</RelativeLayout>
|
Note how the componet is used and that attributes are set here. Also note the use of the cc tag and its declaration in the header of the layout. The com.example.componentsample is the main package name declared in the AndroidManifest.xml file.
I hope this help you to create nice UI components for android. In case something is not clear, please let me know. Also, any feedback (good or bad) is appreciated.