Wednesday, July 4, 2012

Creating custom components in Android



S. Gökhan Topçu
gtopcu@gmail.com


Android provides you with means of creating your custom components - Views or ViewGroups which can be fully customized to suit your needs. You can go from creating a simple component which subclasses a common widget such as a Button or TextView and only implement custom event handling, to creating custom XML attributes for your component and applying custom drawing. If your custom component feels like it is a customized version of any existing component, you can start by subclassing that widget. If not, you can do everything yourself by starting with the android.view.View class. This provides the ability to define fully custom widgets which look and behave as needed.

You can also create custom ViewGroups in the same manner as widgets, either by extending existing layout managers (GridLayout, RelativeLayout etc) or from scratch by starting with the android.view.ViewGroup class. Usually, your requirements will not require you to define a fully custom ViewGroup, but just a few modifications to the existing ones to perhaps customize widget sizing/positioning for your application or game.

Creating a Custom Button

If you are planning on implementing custom drawing/sizing for your custom widgets, you need to override onMeasure(int widthMeasureSpec, int heightMeasureSpec) and onDraw(Canvas canvas) methods. onMeasure() is the method that will report the widget's width and height to the layout manager it resides in, and you need to call setMeasuredDimension(int width, int height) from inside this method to report the dimensions. Check the View.onMeasure(int w, int h) method's javadoc to understand what you need to return. You can then execute your custom drawing inside the onDraw(Canvas canvas) method. android.graphis.Canvas is pretty similar to its counterpart in Swing, and has methods such as drawRect(), drawLine(), drawString(), drawBitmap() etc. which you can use to draw your component. Below is a very simple custom Button that implements these two methods:


code:
package com.ggit.android.blog.customcomponents
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.widget.Button;

public class CustomButton extends Button {

 public CustomButton(Context context) {
  super(context);
 }

 public CustomButton(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);
 }

 public CustomButton(Context context, AttributeSet attrs) {
  super(context, attrs);
 }
 
 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  setMeasuredDimension(200, 100);
 }
 
 @Override
 protected void onDraw(Canvas canvas) {
  Paint paint = new Paint();
  paint.setARGB(50, 30, 30, 180);
  canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
  paint.setARGB(150, 255, 255, 255);
  paint.setTextSize(20);
  canvas.drawText("Custom Button", 30, 50, paint);
 }
}

main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <com.ggit.android.CustomButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp" />
</LinearLayout>

Output:
Figure 1: Custom Button


This is a trivial example which doesn't take the measure specs reported by the layout manager into account in the onMeasure() method. And other things such as any attributes defined in the layout XML for the button that affects the drawing such as the current style/theme, text size, font etc. are also completely ignored, as the onDraw() method will always draw the same button. In real life, you would need to provide a more concrete implementation taking these into account as well.


Creating a Compound Component

If you need to use a custom layout manager, or create a "compound component" which nests several other probably existing widgets inside to bring combined functionality, or both; you can start by extending ViewGroup or any existing layouts depending on your layout managing needs.

Let's say we use the ListView widget with a TextView as header throughout our project. We can wrap these two into a compound component and re-use instead of defining both widgets inside the layout files repeatedly. To start with, let's define the layout XML for our compound component:

ggit_custom_list.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <TextView
        android:id="@+id/ggitCustomListHeader"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="left|center_vertical"
        android:minHeight="40dp"
        android:textAppearance="@android:style/TextAppearance.Small"
        android:textColor="#0099cc"
        android:textStyle="bold"
        android:typeface="serif"
        android:paddingTop="2dp"  
     android:paddingBottom="2dp"  
     android:paddingLeft="8dp"
     android:paddingRight="8dp"/>
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#0099cc"/>
    <ListView 
        android:id="@+id/ggitCustomList"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:divider="#414141"
        android:dividerHeight="1dp"/>
    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#414141"/>
</LinearLayout>

As you can see, we have started by extending LinearLayout and defined our two widgets inside. If you want to define custom XML attributes for your custom widgets/layouts/components, you need to define "styleable" variables in an XML and place it in the /res/values/ directory. For our example, we want to be able to define the header text for our compound component from XML, so let's define a custom attribute:

/res/values/attributes_styleable.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomListViewWithHeader">
        <attr name="headerText" format="string"/>
    </declare-styleable>
</resources>

Styleable resources take the name of the class name of your custom component, and define the attributes as a pair of name/format values. Format can be string, boolean, integer etc.

Once you define your attributes, you should be able to process them in your custom component class and apply them as necessary. These attributes are passed into the constructor using an AttributeSet object. Our custom component's class expects the "headerText" attribute and applies it as the text for the TextView widget if it's found. It also exposes both widgets through getter methods:

CustomListViewWithHeader.java
package com.ggit.android.blog.customcomponents;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;

public class CustomListViewWithHeader extends LinearLayout {

 private TextView listHeader;
 private ListView list;
 
 public CustomListViewWithHeader(Context context, AttributeSet attrs) {
  super(context, attrs);
  LayoutInflater inflater = 
    (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  View view = (View)inflater.inflate(R.layout.ggit_custom_list, this);
  
  TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomListViewWithHeader);
  String headerText = a.getString(R.styleable.CustomListViewWithHeader_headerText);
  a.recycle();
  
  listHeader = (TextView)view.findViewById(R.id.ggitCustomListHeader);
  list = (ListView)view.findViewById(R.id.ggitCustomList);
  if(headerText != null) {
   listHeader.setText(headerText);
  }
 }
 
 public TextView getListHeader() {
  return listHeader;
 }
 
 public ListView getListView() {
  return list;
 }

}

We first inflate our custom layout file and use it as the LinearLayout's layout by passing this to the inflate() method as the second parameter. Then we convert our attributes and styleable variables into a TypedArray, from which we read the XML attribute headerText and set it as the text for our TextView. As you can see, the "R" class generated by the Android tools provide access to the styleable resources and attributes through R.styleable.* integers.

Now that our custom class is also ready to inflate our custom layout and accept custom XML attributes, we can define a layout for our activity which now contains our custom component:

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res/com.ggit.android.blog.customcomponents"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <com.ggit.android.blog.customcomponents.CustomListViewWithHeader
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        custom:headerText="LIST HEADER"/>

</LinearLayout>

As you can see, we have defined a custom:headerText attribute, and our widget is referenced by its full class name since it's not one of the default Android widgets inside the layout XML. We have also added a new namespace: xmlns:custom="http://schemas.android.com/apk/res/com.ggit.android.blog.customcomponents" which tells the compiler where these custom attributes are defined. This namespace must reference the package of the "R" generated class, since our styleable variables are defined inside this class.


We can now use this layout for our activity and access the widgets inside through the getter methods we have defined inside our custom class:

MainActivity.java
package com.ggit.android.blog.customcomponents;

import android.app.Activity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;

public class MainActivity extends Activity {

 private String[] movies = { "Usual Suspects", 
        "Ice Age 4", 
        "Godfather", 
        "Leon the Professional",
        "Avatar",
        "Die Welle",
        "Donnie Darko",
        "Jeux d'Enfants",
        "Hugo",
        "Resident Evil",
        "StarWars",
        "The Girl With The Dragon Tattoo",
        "The Shawshank Redemption"
        };
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        
        
        CustomListViewWithHeader customList = (CustomListViewWithHeader)findViewById(R.id.list);
  
        TextView text = customList.getListHeader();
        ListView list = customList.getListView();
  list.setAdapter(new ArrayAdapter<String>( this,
             android.R.layout.simple_list_item_1,
             movies));
    

    }
}

Output:
Figure 2: Custom component including a ListView and a TextView as header

The output shows that TextView's text is set successfully as we defined in custom:headerText="LIST HEADER". 

Defining custom components is a fun but tricky job. Many apps from well-known brands define their set of custom components to give a unique look to their UI which also matches the company colors, fonts, etc.


Further Reading:
API Guides: Custom Components
android.view.View
android.view.ViewGroup
android.graphics.Canvas

No comments:

Post a Comment

Please leave your feedback below if you found this blog useful, thanks.