How to create custom views in android?

Parminder Singh
MindOrks
Published in
11 min readSep 19, 2018

Before diving into the process of creating a custom view, It would be worth stating why we may need to create custom views.

  • Uniqueness: Create something that cannot be done by ordinary views.
  • Optimisation: A lot of times we tend to add multiple views or constraints to create the desired view that can be optimized drastically in terms of draw, measure or layout time.

The best way to start would be to understand how android manages view groups and lays out views on the screen. Let us take a look at the diagram below.

This is the basic android view life cycle

onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
Every parent view passes a height and width constraint to its child view based on which the child view decides how big it wants to be. The child view then calls setMeasuredDimension() to store its measured width and height.

How are these constraints passed?

Android uses a 32-bit int called the measure spec to pack a dimension and its mode. The mode is a constraint and can be of 3 types:

  • MeasureSpec.EXACTLY: A view should be absolutely the same size as dimension passed along with spec. Eg. layout_width= “100dp”, layout_width=”match_parent”,layout_weight=”1".
  • MeasureSpec.AT_MOST: A view can have maximum height/width of dimension passed. However, it can be also smaller if it wishes to be. Eg android:layout_width=”wrap_content”
  • MeasureSpec.UNSPECIFIED: A view can be of any size. This is passed when we are using a ScrollView or ListView as our parent.

onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int)
Android applies any offsets or margins and calls this method to inform your view about where exactly it would be placed on the screen. Unlike onMeasure, it is called only once during the traversal. So it is recommended to perform any complex calculations in this method.

onDraw(canvas: Canvas)
Finally, Android provides you with a 2D drawing surface i.e the canvas on which you can draw using a paint object.

The UI thread then passes display lists to render thread which does a lot of optimizations and finally GPU process the data passed to it by render thread.

How to define attributes for your view?

Declaring XML attributes is simple. You just need to add a declarable-style in your attrs.xml and declare a format for every attribute.
For instance, if you are creating a simple view which displays a circle with its label. Your attributes may look like this.

<declare-styleable name="CustomView">
<attr name="circleRadius" format="dimension" />
<attr name="circleLabel" format="string"/>
<attr name="showLabel" format="boolean"/>
<attr name="circleColor" format="color"/>
<attr name="circleDrawable" format="reference"/>
<attr name="circleColorType">
<flag name="none" value="0" />
<flag name="fill" value="1" />
<flag name="stroke" value="2" />
<flag name="fillStroke" value="3" />
</attr>
</declare-styleable>

The same is referenced while creating a view in the following manner.

app:circleRadius="5dp"
app:circleDrawable="@drawable/ic_bell"
app:circleColorType="fillStroke"
...

Now, we have to parse these attributes in your java or kotlin class.

  • Create your view class which extends the android.view class
  • Obtain a reference to the attributes declared in XML. While attrs is passed in the constructor, the second parameter is a reference to the styleable we just declared. The latter two are used for getting default style attributes in theme or supplying a default style attributes.
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.YourCustomViewClass, 0, 0)
  • Parsing the attribute arguments
radi= a.getDimension(R.styleable.CustomView_circleRadius,fallback)
showLabel= a.getDimension(R.styleable.CustomView_showName,fallback)
colorType= a.getInteger(R.styleable.CustomView_colorType, colorType)

Android automatically handles the process of converting dp or sp to the right amount of pixels according to screen size when parsing a dimension attribute. But, You need to ensure that the fallback value is converted to appropriate pixel value since android returns fallback value without any conversions if an attribute value is not defined in XML.

While parsing all other attributes is quite straightforward. I will brief you about how to parse flags. Declaring flags attributes can be really useful sometimes since we can check for multiple properties using a single attribute. This is the same way android handles the visibility flag.

colorType here is an integer which represents a flagSet. Now, since every bit in an integer can be used to represent an indication. We can check if a flag exists and perform our operations accordingly. To check if a flag type stroke exits, we can simply perform an or operation on flagSet with the stroke value. If the result stays the same that means the flag actually exists in the flagSet.

  • Finally, recycle the typed array to be used by the later caller.
a.recycle()

Initialising your objects
It is always better to initialize your paint and path objects or other variables in the constructor itself. Since declaring it any other traversal method may result in the meaningless creation of objects again and again.

Calculating your view size

Calculating view size can be really challenging sometimes. You have to make sure that your view does not take any extra pixel or request any less pixel as it may end up showing extra white space or not showing complete view respectively. These are the basic steps that you need to follow to calculate the size of your view.

  • Calculate how much width and height your view requires. For instance, if you are drawing a simple circle with its label below the circle. The suggested width would be :
    (circle diameter+ any extra width if occupied by the label).
  • Calculate the desired width by adding the suggested width with paddingStart and paddingEnd. Similarly, desiredHeight would be suggested height plus paddingTop & paddingBottom.
  • Calculate actual size respecting the constraints. To calculate this, you simply need to pass measure spec passed to you in onMeasure() and your desired dimension in this method called resolveSize(). This method would tell you closest possible dimension to your desired width or height while still respecting its parent's constraints.
  • Most importantly, you need to set the final width and height in onMeasure method by calling setMeasuredDimension(measuredWidth,measuredHeight) to store the measured width and height of this view otherwise, you might see your view crashing with an IllegalStateException.

Positioning your views

We can position our child views by using the onLayoutMethod. The code simply may involve iterating over any child views and assigning them a left, top, right and a bottom bound depending on measured widths and heights.

Drawing your view

Before using the canvas there are few things that we need to understand:

  • Paint: The Paint class holds the style and color information about how to draw geometries, text, and bitmaps. Here is how we create a paint object.
mPaint = new Paint();
mPaint.setColor(mDrawColor);
mPaint.setStyle(Paint.Style.STROKE); // default: FILL
//Smoothes edges of what is drawn without affecting interior shape.
mPaint.setAntiAlias(true);
//Dithering affects how colours with higher precision than device are down-sampled.
mPaint.setDither(true);
mPaint.setStrokeWidth(mStrokeWidth);

You can read about more about the properties here.

  • Drawing Shapes: You can directly draw shapes like a line, arc, circle etc on the canvas. Let us take a look at the diagram below to gain a better understanding.
Describing how oval, arc and rectangle is drawn on the canvas
override fun onDraw(canvas: Canvas) {

super.onDraw(canvas)
val cx = canvas.width/2
val cy = canvas.height/2

ovalRect.left = cx - circleRadius
ovalRect.top = cy - circleRadius
ovalRect.right = cx + circleRadius
ovalRect.bottom = cy + circleRadius

canvas.drawArc(ovalRect, 0F, 90F, true, mCircleFillPaint)
canvas.drawOval(ovalRect, mCircleStrokePaint)
canvas.drawRect(ovalRect, mRectStrokePaint)

}

Using Paths: Drawing complex shapes with the above methods may get a bit complex so android offers a Path class. With the Path class, you can imagine that you are holding a pen and you can draw a shape, then maybe move to a different position and draw another shape. Finally, when you are done creating a path. You can simply draw the path on the canvas like this. Also, when using paths you can also use different path effects (discussed below in detail). Below, is an example of the shape created using paths.

val cx = canvas.width/2
val cy = canvas.height/2
val angle = 2.0 * Math.PI / 5
//move to pos 1
path.moveTo(
cx + (radii * Math.cos(0.0)).toFloat(),
cy + (radii * Math.sin(0.0)).toFloat())
//draw all lines till pos 5
for (i in 1 until 5) {
path.lineTo(
cx + (radii * Math.cos(angle * i)).toFloat(),
cy + (radii * Math.sin(angle * i)).toFloat())
}
// join pos 5 with pos 1
path.close()
//if you want to add a circle around the polygon using path
// path.addCircle(cx, cy, circleRadius, Path.Direction.CW)
//draw polygon
canvas.drawPath(path, mShapePaint)
  • Path Effects: If you also apply a Corner path effect to your paint object with a certain radius the polygon will look like this. You can also use other path effects like DashPathEffect, DiscretePath etc. To combine two different path effects you can use the ComposePathEffect.
mShapePaint.pathEffect = CornerPathEffect(20f)
Polygon using Corner path effect
  • Drawing Bitmaps: To draw bitmaps on the canvas, you can use
canvas.drawBitmap(bitamp , src, dest , paint)

bitmap: Bitmap that you want to draw on canvas
src: It takes a rect object which specifies the portion of the bitmap you want to draw. This can be null if you want to draw the complete bitmap.
dest: A rect object which tells how much area do you want to cover on the canvas with the bitmap
paint: The paint object with which you want to draw the bitmap

Android automatically does all the necessary scaling or translation to fit the source on destination area.

You can also draw drawables on canvas.

drawable.setBounds(left, top , right , bottom)
drawable.draw(canvas)

Before drawing a drawable, you would need to set bounds to your drawable. The left, top, right and bottom describe the drawable’s size and its position on the canvas. You can find the preferred size for Drawables using getIntrinsicHeight() and getIntrinsicWidth() methods and decide bounds accordingly.

Drawing Texts: Drawing texts can be a bit of pain. Not the drawing itself, but the alignment or measurement of text. This occurs because different characters have different heights and to make it more worse there can be different typefaces too. So to measure a text’s height you would need to calculate specific text bounds for your text like this.

textPaint.getTextBounds(yourText, startIndex, endIndex, rect )

Then, the rect object passed in the end would then contain the text bounds of actual text to be drawn. This way you can calculate the actual height of text to be drawn and set a correct baseline y for your text. To calculate the width of your text you should use textPaint.measureText() as it is more accurate than the width given by paint text bounds (because of the way these methods are implemented in skia library). Alternatively, for ensuring the text is centered horizontally on the canvas you can just set your paint’s alignment to TextAlign.CENTER and pass center point of your canvas in the x coordinate.

canvas.drawText(text, xPos , yPos , paint)

Drawing multiline text: If you want to handle line breaks (\n) or If you have a fixed width to draw a text you can use Static Layout or Dynamic Layout. This would automatically handle all the word breaks or line breaks and also tell you how much height would be needed to draw a text in given width.

//building a static layout
var builder = StaticLayout.Builder.obtain(text, 0, text.length(),textPaint , width);
StaticLayout myStaticLayout = builder.build();
var heightRequired = myStaticLayout.height
// the drawing part
canvas.save()
canvas.translate(xPos, yPos)
myStaticLayout.draw(canvas)
canvas.restore()
  • Saving & Restoring Canvas: As you might have noticed, we need to save the canvas and translate it before drawing on it and finally we have to restore the canvas. A lot of times we need to draw something with a different setting such as rotating the canvas, translating it, or clipping a certain part of canvas while drawing a shape. In this case, we can call canvas.save() which would save our current canvas settings on a stack. After this, we change canvas settings ( translation etc) and then draw whatever we want to with these settings. Finally, when we are done drawing we can call canvas.restore() which would restore canvas to the previous configuration that we had saved.
  • Handling User Inputs: Finally, you have created your own custom view using XML attributes, BUT what if you want to change any property at runtime such as the radius of the circle, text color etc. You would need to inform Android API's to reflect the changes. Now, if any change in property affects the size of your view you will set the variable and call requestLayout() which would recalculate your view’s size and redraw it. However, if a property like a text color is changed you would only need to redraw it with new text paint color and in this case, it would be wise to just call invalidate().

Additional Note: Now if your view has a lot of attributes, there may be a lot of times you would have to write invalidate()/requestLayout after every setter. This problem can be solved by using kotlin’s delegates. Let us take a look a the example below to be more clear.

var textColor: Int by OnValidateProp(Color.BLACK)
var circleColor: Int by OnValidateProp(Color.CYAN)
class OnValidateProp<T>(private var field: T, private inline var func: () -> Unit = {}) {
operator fun setValue(thisRef: Any?, p: KProperty<*>, v: T) {
field = v
invalidate()

}

operator fun getValue(thisRef: Any?, p: KProperty<*>): T {
return field
}

}

Now, If I know that a property if changed should only redraw the view, I would initialize it using OnValidateProp but if it can affect the size of the view I would initialize by creating a new OnLayoutProp delegate.

Finally! You can start by creating your own custom views. If you are interested to see what an actual custom view code looks like. You can check out the library that I just published. It displays steps along with the descriptions and covers most of the things that I have discussed in this article.

In conclusion, I will mention some of the best practices that you should always consider to optimize your custom view’s performance.

  • Invalidate intelligently: Do not call invalidate unless and until something visible to the user has changed. Secondly, when you are calling invalidate if possible pass a rect object in invalidate method to tell the GPU what part of the screen needs to be drawn.
  • Draw carefully: Do not draw things that are not visible to the user. After all its a 2D surface and it would be useless drawing something which is later overlapped by something else. You can achieve this by using Canvas.clipRect(). Also, do not draw something that is out of the screen bounds. You can achieve this by using canvas.quickReject().
  • Never allocate objects in onDraw: onDraw() gets called 60 times a second. although garbage collectors are really fast so there may not be a GC related drop but they run on a separate thread, which would mean that you might be eating a lot of battery. Moreover, since most of the time the type of objects initialized in onDraw are drawing objects (wrappers around C++). It leads to calling a lot of destructors and hence running finalizers to reclaim object’s memory which is never good for performance.

--

--