Friday, February 15, 2013

Creating an Android Component

Android API allows one to create custom UI components by several means. For instance, one can override an existing Component, one can create a completely custom view component, or one can combine components to create a new one. The latter will be discussed here.

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:
  1. 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.
  2. 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.
  3. If one wishes to add attributes to be set in the layout XML file which uses the component, follow the steps below:
    1. 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.
    2. 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.
  4. 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.

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.




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.