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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Responsive View

Currently, your custom view has a fixed size, but you want it to be responsive and fit its parent. Also, you want the happy face to always be a circle, not an oval shape.

Android measures the view width and heigh. You can get these values by using measuredWidth, measuredHeight.

Override the onMeasure() method to provide an accurate and efficient measurement of the view contents:


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {      super.onMeasure(widthMeasureSpec, heightMeasureSpec)

}

Add the following lines of code to onMeasure():

// 1
size = Math.min(measuredWidth, measuredHeight)
// 2
setMeasuredDimension(size, size)

Here you:

  1. Calculate the smaller dimension of your view
  2. Use setMeasuredDimension(int, int) to store the measured width and measured height of the view, in this case making your view width and height equivalent.

Build and run the app, and you should see a screen like this:

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:

  1. Open the declare-styleable tag and set the name attribute to your custom view class name.
  2. 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:

  1. Add two constants, one for the HAPPY state and one for the SAD state.
  2. Setup default values of the XML attribute properties, in case a user of the custom view does not set one of them
  3. Add a new property called happinessState for the face happiness state.
  4. Call the invalidate() method in the set happinessState method. The invalidate() method makes Android redraw the view by calling onDraw().
  5. Call a new private setupAttributes() method from the init block.
  6. Obtain a typed array of the XML attributes
  7. Extract custom attributes into member variables
  8. 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:

  1. Draw a happy mouth path by using quadTo() method as you learned before.
  2. 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:

  1. Set the emotionalFaceView‘s happinessState to HAPPY when the user clicks on the happy button.
  2. Set the emotionalFaceView‘s happinessState 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:

Ahmed Tarek

Contributors

Ahmed Tarek

Author

Arun Sasidharan

Tech Editor

Joe Howard

Final Pass Editor

Over 300 content creators. Join our team.