Android Custom View Tutorial
Create an Android Custom View in Kotlin and learn how to draw shapes on the canvas, make views responsive, create new XML attributes, and save view state. By Ahmed Tarek.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android Custom View Tutorial
25 mins
- Getting Started
- Working with the Basic Widgets
- Working with Views in Kotlin
- Working with Views in XML
- Android Views
- Custom View and Custom ViewGroup
- How Android Draws Views
- Creating a custom view
- Android View Class Constructors
- Drawing on Canvas
- Responsive View
- Creating Custom XML Attributes
- User Interaction
- Saving View State
- Where To Go From Here?
Creating Custom XML Attributes
To create a new XML attribute go to res/values and create new values resource file named attrs.xml. Add the following lines to the file:
<!--1-->
<declare-styleable name="EmotionalFaceView">
<!--2-->
<attr name="faceColor" format="color" />
<attr name="eyesColor" format="color" />
<attr name="mouthColor" format="color" />
<attr name="borderColor" format="color" />
<attr name="borderWidth" format="dimension" />
<attr name="state" format="enum">
<enum name="happy" value="0" />
<enum name="sad" value="1" />
</attr>
</declare-styleable>
Here you:
- Open the
declare-styleable
tag and set thename
attribute to your custom view class name. - Add new attributes with different names and set their
format
to a suitable format.
Go to res/layout/activity_main.xml and add the following new views to the RelativeLayout:
<com.raywenderlich.emotionalface.EmotionalFaceView
android:id="@+id/happyButton"
android:layout_width="@dimen/face_button_dimen"
android:layout_height="@dimen/face_button_dimen"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
app:borderColor="@color/white"
app:eyesColor="@color/white"
app:faceColor="@color/red"
app:mouthColor="@color/white"
app:state="happy" />
<com.raywenderlich.emotionalface.EmotionalFaceView
android:id="@+id/sadButton"
android:layout_width="@dimen/face_button_dimen"
android:layout_height="@dimen/face_button_dimen"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
app:borderColor="@color/black"
app:eyesColor="@color/black"
app:faceColor="@color/light_grey"
app:mouthColor="@color/black"
app:state="sad" />
You have added two EmotionalFaceView
objects to the layout, and are using the new custom XML attributes. This proves the reusability concept for the custom view.
The first view has a happy state
and the second view has a sad state
. You will use both of them later to act as buttons with different themes and different happiness states, and
Build and run the app, and you should see a screen like this:
As you can see, the new XML
attributes have no effect yet on the EmotionalFaceView
. In order to receive the values of the XML
attributes and to use them in the EmotionalFaceView
class, update all the lines of code setting up the properties above onDraw()
to be:
// 1
companion object {
private const val DEFAULT_FACE_COLOR = Color.YELLOW
private const val DEFAULT_EYES_COLOR = Color.BLACK
private const val DEFAULT_MOUTH_COLOR = Color.BLACK
private const val DEFAULT_BORDER_COLOR = Color.BLACK
private const val DEFAULT_BORDER_WIDTH = 4.0f
const val HAPPY = 0L
const val SAD = 1L
}
// 2
private var faceColor = DEFAULT_FACE_COLOR
private var eyesColor = DEFAULT_EYES_COLOR
private var mouthColor = DEFAULT_MOUTH_COLOR
private var borderColor = DEFAULT_BORDER_COLOR
private var borderWidth = DEFAULT_BORDER_WIDTH
private val paint = Paint()
private val mouthPath = Path()
private var size = 0
// 3
var happinessState = HAPPY
set(state) {
field = state
// 4
invalidate()
}
// 5
init {
paint.isAntiAlias = true
setupAttributes(attrs)
}
private fun setupAttributes(attrs: AttributeSet?) {
// 6
// Obtain a typed array of attributes
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.EmotionalFaceView,
0, 0)
// 7
// Extract custom attributes into member variables
happinessState = typedArray.getInt(R.styleable.EmotionalFaceView_state, HAPPY.toInt()).toLong()
faceColor = typedArray.getColor(R.styleable.EmotionalFaceView_faceColor, DEFAULT_FACE_COLOR)
eyesColor = typedArray.getColor(R.styleable.EmotionalFaceView_eyesColor, DEFAULT_EYES_COLOR)
mouthColor = typedArray.getColor(R.styleable.EmotionalFaceView_mouthColor, DEFAULT_MOUTH_COLOR)
borderColor = typedArray.getColor(R.styleable.EmotionalFaceView_borderColor,
DEFAULT_BORDER_COLOR)
borderWidth = typedArray.getDimension(R.styleable.EmotionalFaceView_borderWidth,
DEFAULT_BORDER_WIDTH)
// 8
// TypedArray objects are shared and must be recycled.
typedArray.recycle()
}
Here you:
- Add two constants, one for the
HAPPY
state and one for theSAD
state. - Setup default values of the XML attribute properties, in case a user of the custom view does not set one of them
- Add a new property called
happinessState
for the face happiness state. - Call the
invalidate()
method in theset happinessState
method. Theinvalidate()
method makes Android redraw the view by callingonDraw()
. - Call a new private
setupAttributes()
method from theinit
block. - Obtain a typed array of the
XML
attributes - Extract custom attributes into member variables
- Recycle the
typedArray
to make the data associated with it ready for garbage collection.
Build and run the app, and you should see a screen like this:
As you see in the previous screenshot, the happinessState
still has no effect, and both of the EmotionalFaceView buttons are happy.
At the beginning of the drawMouth()
method, add the following line
mouthPath.reset()
This will reset the path and remove any old path before drawing a new path, to avoid drawing the mouth more than one time while Android calls the onDraw()
method again and again.
You want to make the face happy or sad, according to the state, in drawMouth()
. Replace the mouthPath()
drawing with the following lines of code:
if (happinessState == HAPPY) {
// 1
mouthPath.quadTo(size * 0.5f, size * 0.80f, size * 0.78f, size * 0.7f)
mouthPath.quadTo(size * 0.5f, size * 0.90f, size * 0.22f, size * 0.7f)
} else {
// 2
mouthPath.quadTo(size * 0.5f, size * 0.50f, size * 0.78f, size * 0.7f)
mouthPath.quadTo(size * 0.5f, size * 0.60f, size * 0.22f, size * 0.7f)
}
Here you:
- Draw a happy mouth path by using
quadTo()
method as you learned before. - Draw a sad mouth path.
The whole drawMouth()
method will be like this
private fun drawMouth(canvas: Canvas) {
// Clear
mouthPath.reset()
mouthPath.moveTo(size * 0.22f, size * 0.7f)
if (happinessState == HAPPY) {
// Happy mouth path
mouthPath.quadTo(size * 0.5f, size * 0.80f, size * 0.78f, size * 0.7f)
mouthPath.quadTo(size * 0.5f, size * 0.90f, size * 0.22f, size * 0.7f)
} else {
// Sad mouth path
mouthPath.quadTo(size * 0.5f, size * 0.50f, size * 0.78f, size * 0.7f)
mouthPath.quadTo(size * 0.5f, size * 0.60f, size * 0.22f, size * 0.7f)
}
paint.color = mouthColor
paint.style = Paint.Style.FILL
// Draw mouth path
canvas.drawPath(mouthPath, paint)
}
Build and run the app, and you should see the top right button become a sad face, like the following screenshot:
User Interaction
You can let your user change the happiness state of the center emotional face view by clicking on the top left button to make it happy or by clicking on the top right button to make it sad. First, add the following line of code to the MainActivity
import statements:
import kotlinx.android.synthetic.main.activity_main.*
Kotlin Android Extensions provide a handy way for view binding by importing all widgets in the layout in one go. This allows avoiding the use of findViewById()
, which is a source of potential bugs and is hard to read and support.
Now add the following click listeners to onCreate()
in MainActivity:
// 1
happyButton.setOnClickListener({
emotionalFaceView.happinessState = EmotionalFaceView.HAPPY
})
// 2
sadButton.setOnClickListener({
emotionalFaceView.happinessState = EmotionalFaceView.SAD
})
Here you:
- Set the
emotionalFaceView
‘shappinessState
to HAPPY when the user clicks on the happy button. - Set the
emotionalFaceView
‘shappinessState
to SAD when the user clicks on the sad button.
Build and run the app, and click on the both of buttons to change the happiness state: