Android framework provides a set of base classes and XML tags to create a custom view. For example, say we need to set typeface to our text views. All of the view classes defined in the Android framework extend View. A custom view can also extend View directly, or can extending one of the existing view subclasses.

A custom view must provide a constructor that takes a Context and an AttributeSet object as parameters. The constructor allows the layout editor to create and edit an instance of custom view. AttributeSet is a collection of attributes. Attributes are elements defined in XML through which we can set properties of the custom view and control its appearance.

Define Custom Attributes

To define custom attribute to a view, you must:

  • Define attributes for the custom view in a resource element (<resources>) inside of a <declare-styleable> element.
  • Specify values for the attributes in XML layout
  • Retrieve attribute values at runtime, and apply it to custom view

It’s customary to define custom attributes in res/values/attrs.xml file. Here’s an example of an attrs.xml file.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Custom parameters for IndicatorLayout -->
    <declare-styleable name="IndicatorLayout">
        <attr name="indicatorCount" format="integer"/>
    </declare-styleable>
</resources>

This code declares custom attributes, indicatorCount, that belong to a styleable entity named IndicatorLayout. By convention, name of the styleable entity is same name as the name of the class that defines the custom view. An element has two xml attributes name and format. name is used for referring in code, e.g. R.attr.indicatorCount. format can have different values depending on the ‘type’ of attribute you want.

Now custom attributes, can be uses in layout XML files just like built-in attributes. For example, here’s how to use the attributes defined.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Custom view -->
    <com.aphalaprepsunaa.mahabharat.layout.IndicatorLayout
        android:id="@+id/indicatorLayoutId"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginBottom="@dimen/spacing_normal"
        android:layout_alignParentBottom="true"
        android:gravity="center_horizontal"
        app:indicatorCount="0"/>

</RelativeLayout>

Format Types

Format can have different values depending on the ‘type’ of attribute . Some of the possible values are

  • reference : References another resource id (e.g, “@color/white”)
  • color
  • boolean
  • dimension
  • float
  • integer
  • string
  • fraction
  • enum
  • flag

enum attributes can be defined as follows:

<attr name="enum_attr">
  <enum name="val1" value="1" />
  <enum name="val2" value="2" />
</attr>

Similarly flag attributes can also be defined except the values need to be defined so they can be bit ored together. If attribute is reviously defined you do not specify the format. For example,

<declare-styleable name="CustomView">
  <attr name="android:gravity" />
</declare-styleable>

Create Custom View

All of the attributes in the XML tag are read from the resource bundle and passed into the view’s constructor as an AttributeSet. Use obtainStyledAttributes() to retrieve attribute from AttributeSet. This method passes back a TypedArray array of values that have already been dereferenced and styled. Below example show the custom view class.

package com.aphalaprepsunaa.mahabharat.layout

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import com.aphalaprepsunaa.mahabharat.R

class IndicatorLayout : LinearLayout {

    private var mIndicatorCount: Int = 0
    private var mSelectedPosition: Int = 0

    // Override Constructors
    constructor(context: Context,
                attrs: AttributeSet): super(context, attrs) {
        initIndicators(context, attrs, 0)
    }
    private fun px(dpValue: Float): Int {
        return (dpValue * context.resources.displayMetrics.density).toInt()
    }

    /**
     * attrs are attributes used to build the layout parameters
     */
    private fun initIndicators(context: Context, attrs: AttributeSet, defStyleAttr: Int) {

        /**
         * Get TypedArray holding the attribute values in set that are listed in attrs.
         * Default style specified by defStyleAttr and defStyleRes
         * defStyleAttr contains a reference to a style resource that supplies defaults values for attributes
         * defStyleRes is resource identifier of a style resource that supplies default values for the attributes,
         * used only if defStyleAttr is 0 or can not be found in the theme. Can be 0 to not look for defaults.
         */
        val typedArray = context.obtainStyledAttributes(attrs,
            R.styleable.IndicatorLayout,
            defStyleAttr,
            0)

        try {
            mIndicatorCount = typedArray.getInt(R.styleable.IndicatorLayout_indicatorCount, 0)
        } finally {
            typedArray.recycle()
        }

        updateIndicators()
    }

    fun setIndicatorCount(count: Int) {
        mIndicatorCount = count
        updateIndicators()
    }

    private fun updateIndicators() {

        // Remove all child views from the ViewGroup.
        removeAllViews()

        for (i in 0 until mIndicatorCount) {

            // Get view
            val indicator = View(context)

            // Setting indicator layout margin
            val layoutParams = LayoutParams(px(10f), px(10f))
            layoutParams.setMargins(px(3f), px(3f), px(3f), px(3f));
            indicator.layoutParams = layoutParams

            indicator.setBackgroundResource(R.drawable.indicator_unselected)

            // Add the view to indicator layout
            addView(indicator)
        }
    }

    fun selectCurrentPosition(position: Int) {

        if (position in 0..mIndicatorCount) {

            for (index in 0 until mIndicatorCount) {

                //  Child view at the specified position in the group.
                val childView = getChildAt(index)

                if (index == position) {
                    mSelectedPosition = position
                    childView.setBackgroundResource(R.drawable.indicator_selected)
                } else {
                    childView.setBackgroundResource(R.drawable.indicator_unselected)
                }
            }
        }
    }
}

In this example custom view IndicatorLayout extend existing view LinearLayout. It constructor takes AttributeSet as one of the parameter. It update the custom attribute indicatorCount as defined above.