Android custom View and its practical application in the project (continuous update)


Android custom controls are widely used in the project. Many cool looking special effects or the desired effects of the product are not directly provided in the native system controls. At this time, we need custom controls. But the difficulty of custom control is different. When we meet the desired control, we first want to see if there is an open source wheel and use it directly. If we don't, we'll have to find our own way to make it. The process of making wheels may not be easy, so we need to understand the principle and process of customization. This paper summarizes the flow of custom control and several commonly used implementation methods, and makes a summary combined with the actual application of the project. For your reference only. If you have any questions, please leave a message for discussion.

The difficulty of user-defined View can also be divided into three levels. There are three commonly used implementation methods. From easy to difficult.

1. User defined View classification

1.1 inherit existing control

Inherit the specific control, such as TextView,Button,EditText, etc., and expand the function of its control.

For example, you want to customize the font, display the segmented phone number in the text input box, and so on. (refer to implementation mode I below)

1.2 combine existing controls

Inherit ViewGroup, such as LinearLayout,FrameLayout, etc., to realize more powerful controls.

There is no need to override methods such as onMeasure, onLayout, etc. Such as the implementation of title block. (refer to implementation mode II below)

1.3 rewrite View to realize new control

Inheriting View is the most difficult and powerful function. You need to master the principles and steps of drawing.

Such as the realization of countdown entry and so on. (refer to implementation mode 3 below)

2. View coordinate system of Android

First of all, we need to be clear about the acquisition of View coordinate system. The coordinate system in Android is different from that in mathematics.

Coordinate system in Android: the upper left corner of the screen is the origin of the coordinate system (0,0), the origin extends to the right in the positive X-axis direction, and the origin extends downward in the Y-axis direction.

X, Y direction:

View coordinate system:

2.1 methods in view

View gets its own coordinates:

  • getTop(): gets the distance from the top of the view itself to the top of the parent container ViewGroup.
  • getBottom(): gets the distance from the bottom of the view itself to the top of the parent container ViewGroup.
  • getLeft(): gets the distance from the left side of the view itself to the left side of the parent container ViewGroup.
  • getRight(): gets the distance from the right side of the view itself to the left side of the parent container ViewGroup.

View gets its own width and height:

  • getHeight(): get the height of the View
  • getWidth(): get the width of the View

2.2 methods in motionevent

  • getY(): get the x-axis coordinate of the click event relative to the left side of the control, that is, the distance between the click event and the left side of the control.
  • getY(): get the y-axis coordinate of the click event relative to the top edge of the control, that is, the distance between the click event and the top edge of the control.
  • getRawX(): get the x-coordinate of the click event relative to the left side of the whole screen, that is, the distance between the click event and the left side of the whole screen.
  • getRawY(): get the y-axis coordinate of the click event relative to the top edge of the whole screen, that is, the distance between the click event and the top edge of the whole screen.

3. User defined View process

The drawing of View is basically completed by three functions: onMeasure(), onLayout(), onDraw()

function Effect correlation method
onMeasure() Measure the width and height of the View setMeasuredDimension(),onMeasure()
onLayout() Calculate the location of the current View and child views onLayout(),setFrame()
onDraw() Drawing of views onDraw()

For example, we can customize CustomView in the code

class CustomView : View {

    constructor(context: Context?) : super(context)
     * Called automatically when used in an xml layout file
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
     * It does not automatically call, and if there is a default style, it is called in the second constructor.
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :        super(context, attrs, defStyleAttr)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
       override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)

    override fun onDraw(canvas: Canvas?) {

The above code mainly uses the construction method and rewriting method to achieve the desired effect.


 1. The constructor is the entry of View, which is used to initialize some content and get custom properties. 
  • Whether we inherit system View or directly inherit View, we need to rewrite the constructor.
  • There are multiple constructors to be compatible with the lower version. Now in the project, the third function is usually rewritten.
  1. In the actual project, in addition to the constructor, the other three methods are optional.

    In other words, although the drawing process has three methods, namely onMeasure(), onLayout(), onDraw(), it doesn't need to be rewritten, just change the required method

    Next, give an example

4. User defined View used in the project

  • Segment display of phone number input box
  • Encapsulate the general title block
  • Achieve countdown progress bar

4.1 implementation mode 1: user defined EditText

For example, the input telephone number is displayed in sections.

//Enter the phone number and display it in segments, such as: xxx xxxx xxxx
class TelEditText : EditText {
    var isBank = true
    private val addString = " "
    private var isRun = false

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attributes: AttributeSet?) : super(context, attributes) {

    private fun init() {
        addTextChangedListener(object : TextWatcher {
            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {


            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
                //These sentences need to be added. Otherwise, each input value will execute onTextChanged() twice, resulting in stack overflow
                if (isRun) {
                    isRun = false
                isRun = true
                if (isBank) {
                    var finalString = ""
                    var index = 0
                    val telString = s.toString().replace(" ", "")
                    if (index + 3 < telString.length) {
                        finalString += telString.substring(index, index + 3) + addString
                        index += 3
                    while (index + 4 < telString.length) {
                        finalString += telString.substring(index, index + 4) + addString
                        index += 4
                    finalString += telString.substring(index, telString.length)
                    //This sentence is indispensable, otherwise the input cursor will appear on the leftmost side and will not move with the input value to the right

            override fun afterTextChanged(s: Editable) {


    // Get phone number without spaces
    fun getPhoneText(): String {
        val str = text.toString()
        return replaceBlank(str)

    private fun replaceBlank(str: String?): String {
        var dest = ""
        if (str != null) {
            val p = Pattern.compile("\\s*|\t|\r|\n")
            val m = p.matcher(str)
            if (m.find()) {
                dest = m.replaceAll("")
        return dest

In the layout interface, call:

        tools:text="13609213770" />

It is similar to EditText in use, except for the function expansion of properties and display.

4.2 implementation mode 2: custom title block

The renderings are as follows:

1. First draw the style of the title in xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android=""

        android:src="@drawable/common_back_white" />

        tools:text="title" />

        tools:visibility="visible" />

        tools:visibility="visible" />


2. Custom attribute attr.xml

<?xml version="1.0" encoding="utf-8"?>
    <declare-styleable name="TitleLayout">
        <attr name="title_layout_background" format="color" />
        <attr name="title_layout_statusBarBackground" format="color" />
        <attr name="title_layout_titleText" format="string" />
        <attr name="title_layout_titleSize" format="dimension" />
        <attr name="title_layout_titleColor" format="color" />
        <attr name="title_layout_rightText" format="string" />
        <attr name="title_layout_rightTextSize" format="dimension" />
        <attr name="title_layout_rightTextColor" format="color" />
        <attr name="title_layout_rightImageSrc" format="reference|color" />


3. Implementation in code

 * Custom title block control
class TitleLayout : FrameLayout {

    // Default background color
    private val defaultBackgroundColor = ResourcesUtil.getColor(R.color.common_black_1F)
    // Whether to interrupt and return to the previous interface? This value is called after returning to listen
    private var isInterruptBack = false
    // Return to click monitoring
    private var backClickListener: ((View) -> Unit)? = null

    constructor(context: Context) : this(context, null, 0)

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    //Implemented in this function
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
    ) {
        LayoutInflater.from(context).inflate(R.layout.common_include_title, this, true)

    fun setInterruptBack(interrupt: Boolean): TitleLayout {
        isInterruptBack = interrupt
        return this

    fun setOnBackClickListener(click: (View) -> Unit): TitleLayout {
        backClickListener = click
        return this

    fun getBackImageButton(): ImageButton = ibBack

    fun hideBackImage() {

    fun getTitleTextView(): TextView = tvTitle

    fun setTitleColor(color: Int): TitleLayout {
        return this

    fun setTitleSize(size: Float): TitleLayout {
        tvTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
        return this

    fun setTitleText(text: CharSequence): TitleLayout {
        tvTitle.text = text
        return this

    fun setTitleText(@StringRes resId: Int): TitleLayout {
        return this

    fun setLayoutBackgroundColor(color: Int): TitleLayout {
        return this

    fun getRightTextView(): TextView = tvRightText

    fun setRightText(text: CharSequence): TitleLayout {
        if (text.isNotEmpty()) {
            tvRightText.text = text
        return this

    fun setRightTextColor(color: Int): TitleLayout {
        return this

    fun setRightTextSize(size: Float): TitleLayout {
        tvRightText.setTextSize(TypedValue.COMPLEX_UNIT_PX, size)
        return this

    fun setOnRightTextClickListener(click: (View) -> Unit): TitleLayout {
        tvRightText.onClick { click(it) }
        return this

    fun getRightImageButton(): ImageButton = ibRight

    fun setRightImageResource(id: Int) {

    fun setOnRightIconClickListener(click: (View) -> Unit): TitleLayout {
        ibRight.onClick { click(it) }
        return this

    private fun initAttr(attrs: AttributeSet?) {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.TitleLayout)

        val layoutBackgroundColor = typedArray.getColor(

        val statusBarBackgroundColor = typedArray.getColor(
            R.styleable.TitleLayout_title_layout_statusBarBackground, layoutBackgroundColor
        val activity = context
        if (activity is Activity) {
            BarUtil.setStatusBarColor(activity, statusBarBackgroundColor)

        setTitleText(typedArray.getString(R.styleable.TitleLayout_title_layout_titleText) ?: "")
        val titleTextSize =
            typedArray.getDimension(R.styleable.TitleLayout_title_layout_titleSize, -1f)
        if (titleTextSize != -1f) {
        val titleTextColor =
            typedArray.getColor(R.styleable.TitleLayout_title_layout_titleColor, -1)
        if (titleTextColor != -1) {

        setRightText(typedArray.getString(R.styleable.TitleLayout_title_layout_rightText) ?: "")
        val rightTextSize =
            typedArray.getDimension(R.styleable.TitleLayout_title_layout_rightTextSize, -1f)
        if (rightTextSize != -1f) {
        val rightTextColor =
            typedArray.getColor(R.styleable.TitleLayout_title_layout_rightTextColor, -1)
        if (rightTextColor != -1) {

        val rightImageSrc =
            typedArray.getResourceId(R.styleable.TitleLayout_title_layout_rightImageSrc, -1)
        if (rightImageSrc != -1) {


    private fun initBackClickListener() {
        val cxt = context
        ibBack.onClick {
            if (!isInterruptBack && cxt is Activity) {

Call in layout file:

        tools:title_layout_titleText="Title name" />

Call in the Activity interface:

//The title name and event can be handled here

//If the return event is not overridden, the default return event is the close interface 
titleLayout.setOnBackClickListener {
           //Rewrite this method to achieve the desired return logical business

4.3 implementation mode 3: inherit View to rewrite

If the countdown function is realized

The effect is as follows:

Implementation mode in the code:

class RoundProgressBar : View {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        init(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
    ) {

        init(context, attrs)

     * Reference to brush object
    private var paint = Paint()
     * Ring color
    private var roundColor = 0

     * Ring progress color
    private var roundProgressColor = 0

     * Color of string for intermediate progress percentage
    private var textColor = 0

     * Font of string for intermediate progress percentage
    private var textSize = 0f

     * Width of the torus
    private var roundWidth = 0f

     * Maximum progress
    var max = 0

     * The starting angle of the progress circle 0 is three o'clock
    private var startAngle = 0

     * Scanning angle of progress circle
    private var sweepAngle = 0

     * Show progress in the middle
    private var textIsDisplayable = false

     * Progress style, solid or hollow
    private var style = 0

    companion object {
        const val STROKE = 0
        const val FILL = 1

    private fun init(context: Context, attrs: AttributeSet?) {

        val mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundProgressBar)

        // Get custom properties and defaults
        roundColor =
            mTypedArray.getColor(R.styleable.RoundProgressBar_process_roundColor, Color.RED)
        roundProgressColor = mTypedArray.getColor(
        textColor = mTypedArray.getColor(R.styleable.RoundProgressBar_process_txtColor, Color.GREEN)
        textSize = mTypedArray.getDimension(R.styleable.RoundProgressBar_process_txtSize, 15f)
        roundWidth = mTypedArray.getDimension(R.styleable.RoundProgressBar_process_roundWidth, 5f)
        max = mTypedArray.getInteger(R.styleable.RoundProgressBar_process_max, 100)
        startAngle = mTypedArray.getInteger(R.styleable.RoundProgressBar_process_startAngle, 0)
        sweepAngle = mTypedArray.getInteger(R.styleable.RoundProgressBar_process_sweepAngle, 360)
        textIsDisplayable =
            mTypedArray.getBoolean(R.styleable.RoundProgressBar_textIsDisplayable, true)
        style = mTypedArray.getInt(R.styleable.RoundProgressBar_process_style, 0)

     * Set the progress. This is a thread safe control. Due to the consideration of multiple lines, you need to synchronously refresh the interface and call postInvalidate() to refresh in a non UI thread
    var progress = 0
        @Synchronized set(progress) {
            var progress = progress
            if (progress < 0) {
                progress = 0
            if (progress > max) {
                progress = max
            if (progress <= max) {
                field = max - progress


    //Set the color circle of inner ring solid
    var cirCleColor: Int
        get() = roundColor
        set(criCleColor) {
            this.roundColor = criCleColor

    //Generally, you just want to redraw the UI when the View changes. The invalidate() method system automatically calls the onDraw() method of View.
    var cirCleProgressColor: Int
        get() = roundProgressColor
        set(criCleProgressColor) {
            this.roundProgressColor = criCleProgressColor

    override fun onDraw(canvas: Canvas) {

        // Get the x coordinate of the center
        val centre = width / 2
        val radius = (centre - roundWidth / 2).toInt() // Radius of torus

        with(paint) {
            strokeCap = Paint.Cap.ROUND
            isAntiAlias = true // Eliminating sawtooth
            isDither = true //Prevent jitter
            color = roundColor // Set the color of the torus
            style = Paint.Style.FILL // Hollow set
            strokeWidth = roundWidth // Set the width of the torus
        canvas.drawCircle(centre.toFloat(), centre.toFloat(), radius.toFloat(), paint) // Draw circles

        // Limits for the shape and size of arcs used to define
        val oval = RectF(
            (centre - radius).toFloat(),
            (centre - radius).toFloat(),
            (centre + radius).toFloat(),
            (centre + radius).toFloat()

         * Draw an arc, draw the progress of a circle

        when (style) {
            //Current circle
            STROKE -> {
       = Paint.Style.STROKE
                paint.color = roundProgressColor // Set color for progress
                // drawArc - Draw an arc according to the progress
                // The range of the shape and size of the arc defined by the first parameter
                // The second parameter is used to set the angle from which the arc is drawn clockwise. 0 is the three o'clock direction
                // The third parameter is used to set the angle of arc sweeping (anti clockwise is required)
                // The fourth parameter is used to set whether our arc passes through the circle when drawing
                // The fifth parameter is to set the properties of our brush object
                    -(sweepAngle * this.progress / max).toFloat(),
            FILL -> {
       = Paint.Style.FILL_AND_STROKE
                // Draw an arc according to the progress
                if (this.progress != 0) {
                    canvas.drawArc(oval, 0f, (360 * this.progress / max).toFloat(), true, paint)
            else -> {

The way to call in layout:

        app:process_sweepAngle="360" />

Call in the interface:

class MainActivity : AppCompatActivity() {

    companion object {
        private const val DURATION_TIME = (10 * 1000).toLong()
        private const val TOTAL_PROGRESS = 100

    override fun onCreate(savedInstanceState: Bundle?) {

    private val mTimer = object : CountDownTimer(DURATION_TIME, 100) {

        override fun onTick(millisUntilFinished: Long) {
            // Call every second, how much time is left
            val progress = (TOTAL_PROGRESS * (DURATION_TIME - millisUntilFinished) / DURATION_TIME)

            rbProgress.progress = progress.toInt()
            mTvTime.text = "Count down ${millisUntilFinished / 1000}second"

           // completion of enforcement
        override fun onFinish() {
            rbProgress.progress = TOTAL_PROGRESS
            mTvTime.text = "Time out"

    override fun onDestroy() {

Note: when refreshing the interface, choose invalidate() or postInvalidate()?

Let's start with the difference:

  • invalidate()

    This method can only be called in the main UI thread, and will refresh the entire View. When the visibility of this View is VISIBLE, the onDraw() method of View will be called.

  • postInvalidate()

    This method can be called in the non UI thread (any thread) to refresh the UI, not necessarily in the main thread. Because in postInvalidate(), the handler is used to send the message of refreshing the interface to the main thread. Because it is implemented by sending messages, its interface refresh speed may not be as fast as calling invalidate() directly.

    So when we are not sure whether the current refresh interface is in the main thread, it is better to use postInvalidate();

If you can be sure, use invalidate(), for example, onTouchEvent() in the touch feedback event is in the main thread, so it is more appropriate to use invalidate().

5. summary

So far, it summarizes the three implementation methods used in the project. However, what this article shows is only the tip of the iceberg in the custom View, which is worth further exploration by developers.

reference material:

1.Android custom View

2.Android view coordinate system

Tags: Android xml encoding Attribute

Posted on Tue, 12 Nov 2019 02:05:02 -0500 by kaeserea