Logo Icon Logo
A Crowd-sourced Cookbook on Writing Great Android® Apps
GitHub logo Twitter logo OReilly Book Cover Art

Creating a Custom View GUI Component

Published? true
FormatLanguage: WikiFormat

Problem:

You want to create a custom View component

Solution:

Write a class that extends View; implement two methods and two constructors. Describe optional XML attributes in an XML file. Use your new component!

Discussion:

Android provides a large collection of View components, described in this and the following chapters. You can often tailor them, and you can also subclass them to add or change behaviour. In the extreme case, you may want to start from scratch. We needed to build a "gauge" showing how a patient was doing in keeping their weight within a range specified by a caregiver, as in this figure (from the included Demo Activity) which shows low, in-range and high examples, for a person who was supposed to keep their range between 140 and 160 pounds.

The basic approach when starting from scratch is to extend View, the most basic displayable component and the ancestor of all visual controls and displays. You must override two methods to be minimally useful, onMeasure() and onDraw().

The first of these, onMeasure(), is called to do layout. If you are managing multiple child components, you'd typically iterate over them to calculate your required area. In any event, you must call setMeasuredDimension() when done, to tell Android how much space you actually need. In this example, the control is simple, has no children, and simply sets the width and height to the fixed values.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	setMeasuredDimension(mWidth, mHeight);
}

A typical View subclass, managing child components, might want to examine the incoming values, which are declared as int but actually have a request type encoded in them; decode with the static View.MeasureSpec() methods getMode( measureSpec) and getSize(measureSpec); the mode is one of AT_MOST, EXACTLY or UNKOWN. See the (int,%20int) onMeasure() documentation.

The second such method, onDraw(), is called with a Canvas, upon which you do any actual drawing needed, using a 2D Paint object's graphics methods. In our example we use drawRect() to outline the bar and to fill in the colored portion (changing the Paint object's draw style between Stroke and Fill as appropriate). We also use the Paint's drawText() method to output the numbers, drawLine() to draw the tick marks.

Much of the body is made up of simple arithmetic calculations to establish the locations of the scale and reading, the height of the colored portion of the bar, and so on, scaling the ones that are in reading units to be in pixel units. A FontMetrics is used to get the font height so we can draw the text vertically centered beside the tick marks.

	@Override
	protected void onDraw(Canvas canvas) {
	super.onDraw(canvas);
		
	// Label it
	int oneThirdHeight = getPaddingTop() + (2*mHeight/3);
        int twoThirdsHeight = getPaddingTop() + (mHeight/3);
        
        String stringMax = Integer.toString(mMax);
        String stringMin = Integer.toString(mMin);
        String stringValue = Integer.toString(mValue);
	FontMetrics fm = mPaint.getFontMetrics();
	final float fontHeight = fm.ascent + fm.descent;
		
	canvas.drawText(stringMax, 
        	getPaddingLeft(), twoThirdsHeight - fontHeight/2, mPaint);
	canvas.drawText(stringMin, 
        	getPaddingLeft(), oneThirdHeight - (fontHeight/2), mPaint);
        
        // Draw the bar outline, at 1/2 and 2/3 of the width
        int top = getPaddingTop();
        int bot = mHeight - getPaddingBottom();
        int leftSide = (int) (mWidth*0.40f);
        int rightSide = (int) (mWidth*0.60f);
        Style oldStyle = mPaint.getStyle();
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawRect(leftSide, top, rightSide, bot, mPaint);
        mPaint.setStyle(oldStyle);
		
	// Now draw the bar graph.
	// The distance from min to max fills the middle third of the graph
	int oneThirdValue = mMax - mMin;
	int valueRange = oneThirdValue * 3;
	int valueAtBottom = mMin - oneThirdValue;
	int visibleValue = Math.max(0, mValue - valueAtBottom);
	int barHeight = Math.min(mHeight, 
		(int) (mHeight * (1f * visibleValue / valueRange)));
		
	// Put in three tick marks before changing the color
	canvas.drawLine(leftSide-20, oneThirdHeight, leftSide, oneThirdHeight, mPaint);
	canvas.drawLine(leftSide-20, twoThirdsHeight, leftSide, twoThirdsHeight, mPaint);
	canvas.drawLine(rightSide, mHeight - barHeight, rightSide + 20, mHeight - barHeight, mPaint);
		
	mPaint.setColor(isInRange() ? colorInRange : colorOutOfRange);
	canvas.drawRect(leftSide, mHeight - barHeight, rightSide, bot, mPaint);
		
	// Draw the actual reading value beside the bar top
	mPaint.setColor(isInRange() ? colorNeutral : colorOutOfRange);
	canvas.drawText(stringValue, 
		mWidth*0.65f, mHeight - barHeight - (fontHeight/2), mPaint);
	}

Attributes that are to be specified in XML layouts when your component is used in an application, must be specified as "Styleable" in an XML file that is part of your Component, and typically named "res/values/attrs.xml". Here we declare that the min and max values of the range can be specified (though in real life these are set from code as they are per-patient), an optional orientation (which isn't implemented yet) and the three color values used for neutral (the box and the scale text) and the "ok" and "out of range" color for the bar graph.

<resources>
   <declare-styleable name="RangeGraph">
       <!-- Allow optional setting of min and max from the XML layout -->
       <attr name="minimum" format="integer"/>
       <attr name="maximum" format="integer"/>
       <!-- Provide an Orientation parameter as an enum - Not used yet! -->
       <attr name="orientation" format="enum">
           <enum name="vertical" value="0"/>
           <enum name="horizontal" value="1"/>
       </attr>
       <!-- Allow to override the default colors -->
       <attr name="colorNeutral" format="color" />
       <attr name="colorInRange" format="color" />
       <attr name="colorOutOfRange" format="color" />
   </declare-styleable>
</resources>

We need to write two constructors, one which just takes a Context argument (for use where code-based layout is used), and the other which adds an AttributeSet argument used to pass attributes in from the XML.

	public RangeGraph(Context context) {
		super(context);
		commonSetup();
	}

	/**
	 * @param context
	 * @param attrs
	 */
	public RangeGraph(Context context, AttributeSet attrs) {
		super(context, attrs);
				
		// Then allow overrides from XML
		TypedArray a = context.getTheme().obtainStyledAttributes(
				attrs,
				R.styleable.RangeGraph,
				0, 0);

		try {
			colorNeutrala.getColor(
			R.styleable.RangeGraph_colorNeutral, Color.BLACK);
			colorInRange = a.getColor(
			R.styleable.RangeGraph_colorInRange, Color.GREEN);
			colorOutOfRange = a.getColor(
			R.styleable.RangeGraph_colorOutOfRange, Color.RED);
			mMin = a.getInteger(R.styleable.RangeGraph_minimum, 0);
			mMax = a.getInteger(R.styleable.RangeGraph_maximum, 100);
		} finally {
			a.recycle();
		}

		commonSetup();
	}

The AttributeSet should be converted to a TypedArray as shown here, and values extracted from it (if you try to access the AttributeSet directly you will have to do a great deal more work).

We use the new Component in an Activity's XML layout by using its full class name in place of the abbreviations like Button or TextView:

<com.example.rangegraph.RangeGraph
        android:id="@+id/lowGauge"
        style="@style/RoundedBorderHolo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

Note that the RangeGraph does not include any border drawing code; the nice rounded border shown in the screenshots is provided by the superclass View method and a bit of XML which defines a style and a Drawable.

In the following example we add a custom color to one of the RangeGraph components. We use an XML Namespace of our package:

<SomeLayout 
        xmlns:rg="http://schemas.android.com/apk/res/com.example.rangegraph"
        ...>
<com.example.rangegraph.RangeGraph
        android:id="@+id/highGauge"
        style="@style/RoundedBorderHolo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        rg:colorOutOfRange="@android:color/holo_purple"/>

Note that because we have defined the attributes in the XML, the ADT XML editor allows us to use code completion on the name of the attribute and in some cases on its value.

The few miscellaneous methods not discussed here are shown in the code example, and should be self-explanatory. N.B. Any set() method that causes a change to the layout must call invalidate() and requestLayout() after changing fields used in layout calculations.

See Also:

Writing your own Layout Manager, 4135.

Download:

The source code for this project is in the Android Cookbook repository, http://github.com/IanDarwin/Android-Cookbook-Examples/,in the subdirectory RangeGraphDemo.