Learning Custom Views in Android

I just created my first custom view, and I would like to share what I learned and hopefully this can help someone to create their own custom view. Basically, this custom view is simple analog clock view, where we can put in our xml then it will just work. Here is the screenshot

It’s available in github and everyone can use this as library

https://github.com/huteri/analogclock

Let’s see the components I used to create the view.

1. Canvas.drawArc()

This is a way to draw an arc. We can draw complete circle with this method. From the documentation we can see this

1
2
drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
// Draw the specified arc, which will be scaled to fit inside the specified oval.

In an circle we have angle, so startAngle is starting point to draw the arc. SweepAngle is the how long the arc will be. The zero in startAngle starts from 3 o'clock, so if we put -90 as startAngle, and 180 as sweepAngle, then android will draw half oval from 12 o'clock to 6 o'clock.

So, what is RectF in this case? It’s like the canvas where we want to draw the circle. The arc will follow the form of this rectF, to draw a circle we will need to have a square as rectF which has 4 equal sides.

1
2
3
4
5
6
7
8
9
rectF.apply {
  set(centerX - 400, centerY - 400, centerX + 400, centerY + 400)
}

it.drawArc(rectF, -90f, 180f, false, paint.apply {
  color = Color.parseColor("#95a5a6")
  strokeWidth = 20f
  style = Paint.Style.STROKE
})

We create a rectF with size 400 in all 4 sides, then draw an arc inside of this

2. Draw Moving Hands in Analog Clock

We have moving hands in the analog clock for hour, minute, and second. How to draw this? Basically the hands are just rectangular with rounded corners. We can draw rounded rectangular easily with Canvas.drawRoundRect() api. But how to draw rounded rect to point to 5 o'clock? This is where Canvas.save() and Canvas.rotate() come to play.

We basically save the snapshot of the current canvas with Canvas.save(). I would say this thing saves the position of each view inside of the canvas. First we draw the rounded rectangular to 12 o'clock, then rotate the canvas 150 degrees to get it to 5 o'clock. (every hour is 30 degrees)

Call Canvas.restore to restore position of each view from last save. it won’t restore the rounded rect because we save before we draw this.

3. Touch Area and Selected Area

This is one the hardest part in this custom view. This is about how to make our custom view clickable and respond to our touch.

So, how do we define touch area for specific view? By using Region. Defining a region is basically the same like we draw a view. We can use the arc as the region, but we should make the region in such way that the user can easily touch and select the item.

That’s why we need Path instead of only an arc, because with Path we can define the touch area we want instead of just an arc.

Look on the screenshot for custom view again, we want to make sure that user can easily select the data. So we are going to make touch area from the center of the view to arc. It’s like slice of pizza. Here is to give you picture.

The slices with grey background are the touch area, so whenever user touches inside of this area, the item will be selected.

Remember, in order to define touch area, we need to know how to draw it. Let’s see how we can define this area.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
val centerX = (it.width.div(2)).toFloat()
val centerY = (it.height.div(2)).toFloat()

var pathSelected = Path()

rectFSelected.apply {
    set(centerX - 400, centerY - 400, centerX + 400, centerY + 400)
}

pathSelected.arcTo(rectFSelected, startAngle, sweepAngle)

rectFSelected.apply {
    set(centerX, centerY, centerX, centerY)
}

pathSelected.arcTo(rectFSelected, 0f, 0f)
pathSelected.close()

pathSelected.computeBounds(rectFSelected, true)

First, we are going to define the first arc which is the curve of the slice, and then define the second arc, but if you look closely, the rect for second arc is the center of the view which is height and width of the canvas divided by 2. So there is actually no arc. It’s just telling path to use this center view. Path.close() will close those 2 arcs with lines. That’s how we get the slice of the pie.

That’s it. we get the Path and then we only need to set this path as region, and put it inside of the data list.

Override onTouchEvent() then we check whether the touch point is inside of one of the regions, then call sliceClickListener callback then call postInvalidate() to draw the selected area.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 override fun onTouchEvent(event: MotionEvent?): Boolean {

    var point = Point()
    point.x = event!!.x.toInt()
    point.y = event!!.y.toInt()

    var count = 0

    for (arcSlice in list) {
        var region = Region().apply {
            setPath(arcSlice.path, arcSlice.region)
        }

        if (region.contains(point.x, point.y) && event.action == MotionEvent.ACTION_DOWN) {
            indexSelected = count

            onSliceClickListener?.onSliceClick(indexSelected)
            break
        }

        count++

    }

    if (event.action == MotionEvent.ACTION_DOWN || event.action == MotionEvent.ACTION_UP) {
        postInvalidate()
    }

    return true
}

Comments