Material error label component

Today we are going to create re-usable android error label component for EditText as described in Errors category of material design spec.

The idea is to wrap any EditText with custom ViewGroup, let's call it - ErrorLabelLayout. Job of ErrorLabelLayout is to show error label and tint EditText background to red or any other color.

<com.dd.ui.view.ErrorLabelLayout  
    android:id="@+id/nameErrorLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <EditText
        android:id="@+id/editFirsName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="First name"/>

</com.dd.ui.view.ErrorLabelLayout>  

Let's start with creating ErrorLabelLayout class which should extend LinearLayout. Implement all constructors and declare empty initView() method.

public class ErrorLabelLayout extends LinearLayout {

    public ErrorLabelLayout(Context context) {
        super(context);
        initView();
    }

    public ErrorLabelLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public ErrorLabelLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
    // for later use
    }
}

Now it's time to fill initView() method. Here we are doing some basic initialization and listening of onChildViewAdded() event. When we declare our EditText via xml inside ErrorLabelLayout this event is triggered. With getChildAt() method we can get reference of EditText background drawable to tint it when necessary.

public class ErrorLabelLayout extends LinearLayout implements ViewGroup.OnHierarchyChangeListener {

    private Drawable mDrawable;
    private int mErrorColor;

    public ErrorLabelLayout(Context context) {
        super(context);
        initView();
    }

    public ErrorLabelLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public ErrorLabelLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        setOrientation(VERTICAL);
        setOnHierarchyChangeListener(this);
        mErrorColor = Color.parseColor("#D32F2F");
    }

    @Override
    public void onChildViewAdded(View parent, View child) {
        int childCount = getChildCount();
        if (childCount == 1) {
            mDrawable = getChildAt(0).getBackground();
            // add error label here
        }
    }

    @Override
    public void onChildViewRemoved(View parent, View child) {

    }
}

Next step is to init and add error label below EditText. Note that error TextView - is focusable.

public class ErrorLabelLayout extends LinearLayout implements ViewGroup.OnHierarchyChangeListener {

    private static final int ERROR_LABEL_TEXT_SIZE = 12;
    private static final int ERROR_LABEL_PADDING = 4;

    private TextView mErrorLabel;
    private Drawable mDrawable;
    private int mErrorColor;

    public ErrorLabelLayout(Context context) {
        super(context);
        initView();
    }

    public ErrorLabelLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public ErrorLabelLayout2(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        setOrientation(VERTICAL);
        setOnHierarchyChangeListener(this);
        mErrorColor = Color.parseColor("#D32F2F");
        initErrorLabel();
    }

    private void initErrorLabel() {
        mErrorLabel = new TextView(getContext());
        mErrorLabel.setFocusable(true);
        mErrorLabel.setFocusableInTouchMode(true);
        mErrorLabel.setTextSize(ERROR_LABEL_TEXT_SIZE);
        mErrorLabel.setTextColor(mErrorColor);
        mErrorLabel.setPadding(dipsToPix(ERROR_LABEL_PADDING), 0, dipsToPix(ERROR_LABEL_PADDING), 0);
    }

    @Override
    public void onChildViewAdded(View parent, View child) {
        int childCount = getChildCount();
        if (childCount == 1) {
            mDrawable = getChildAt(0).getBackground();
            addView(mErrorLabel);
        }
    }

    @Override
    public void onChildViewRemoved(View parent, View child) {

    }

    private int dipsToPix(float dps) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
            dps, getResources().getDisplayMetrics());
    }
}

Final step is to add helper methods to set / clear error and tint EditText background. The reason why we made error label focusable - is that on Android L EditText background Drawable is not tinted, until EditText remains focus. That's why inside setError() method we switch focus to error label.

public class ErrorLabelLayout extends LinearLayout implements ViewGroup.OnHierarchyChangeListener {

    private static final int ERROR_LABEL_TEXT_SIZE = 12;
    private static final int ERROR_LABEL_PADDING = 4;

    private TextView mErrorLabel;
    private Drawable mDrawable;
    private int mErrorColor;

    public ErrorLabelLayout(Context context) {
        super(context);
        initView();
    }

    public ErrorLabelLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public ErrorLabelLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        setOnHierarchyChangeListener(this);
        setOrientation(VERTICAL);
        mErrorColor = Color.parseColor("#D32F2F");
        initErrorLabel();
    }

    private void initErrorLabel() {
        mErrorLabel = new TextView(getContext());
        mErrorLabel.setFocusable(true);
        mErrorLabel.setFocusableInTouchMode(true);
        mErrorLabel.setTextSize(ERROR_LABEL_TEXT_SIZE);
        mErrorLabel.setTextColor(mErrorColor);
        mErrorLabel.setPadding(dipsToPix(ERROR_LABEL_PADDING), 0, dipsToPix(ERROR_LABEL_PADDING), 0);
    }

    public void setErrorColor(int color) {
        mErrorColor = color;
        mErrorLabel.setTextColor(mErrorColor);
    }

    public void clearError() {
        mErrorLabel.setVisibility(INVISIBLE);
        mDrawable.clearColorFilter();
    }

    public void setError(String text) {
        mErrorLabel.setVisibility(VISIBLE);
        mErrorLabel.setText(text);
        // tint drawable
        mDrawable.setColorFilter(mErrorColor, PorterDuff.Mode.SRC_ATOP);
        // changing focus from EditText to error label, necessary for Android L only
        // EditText background Drawable is not tinted, until EditText remains focus
        mErrorLabel.requestFocus();
    }

    @Override
    public void onChildViewAdded(View parent, View child) {
        int childCount = getChildCount();
        if (childCount == 1) {
            mDrawable = getChildAt(0).getBackground();
            addView(mErrorLabel);
        }
    }

    @Override
    public void onChildViewRemoved(View parent, View child) {
    }

    private int dipsToPix(float dps) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
            dps, getResources().getDisplayMetrics());
    }
}

Now when our ErrorLabelLayout is ready let's create a layout for testing purposes - activity_main.

<LinearLayout  
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center_horizontal"
    android:padding="16dp">

    <com.dd.ui.view.ErrorLabelLayout
        android:id="@+id/nameErrorLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:id="@+id/editFirsName"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="First name"/>

    </com.dd.ui.view.ErrorLabelLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btnError"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="8dp"
            android:text="Show error"/>

        <Button
            android:id="@+id/btnClear"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:text="Clear error"/>

    </LinearLayout>

</LinearLayout>  

And a sample Activity. If you want to make error disappear whenever user click on edit text - use OnTouchListener. Complete code snippet is available on GitHub

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final ErrorLabelLayout errorLabelLayout = (ErrorLabelLayout) findViewById(R.id.nameErrorLayout);

        Button btnError = (Button) findViewById(R.id.btnError);
        btnError.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                errorLabelLayout.setError("First name is required");
            }
        });

        Button btnClear = (Button) findViewById(R.id.btnClear);
        btnClear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                errorLabelLayout.clearError();
            }
        });

        // whenever user click on edit text - remove error
        EditText editFirsName = (EditText) findViewById(R.id.editFirsName);
        editFirsName.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_UP:
                        errorLabelLayout.clearError();
                        break;
                }
                return false;
            }
        });
    }
}

Result

comments powered by Disqus