How can I make the color of an EditText styleable with a custom attribute (like an error)?

469 Views Asked by At

I'd like to make an EditText whose text turns red when an "error" condition is met. I'm aware that TextInputLayout has a built-in error state, but I don't want to have text beneath the input area, and I don't want to include the support design libraries in my project (which is an sdk - so others will include it). However, my EditText always shows the error state, and I'm out of ideas as to why.

My efforts thus far:

To begin, I declare a styleable thing. Seems fine.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MySpecialEditText">
        <attr name="state_error" format="boolean"/>
    </declare-styleable>
</resources>

Then I create a selector in my res/colors directory. Now, I'd really prefer to only override the color if my special attribute is set. But if I have to copy over the android:state-pressed, etc, states and every combination thereof, I can do that (and have, with the same results, see below). This file is res/color/red_on_error_dark.xml.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:custom="http://schemas.android.com/apk/lib/com.mypackage">
    <item
        custom:state_error="true"
        android:color="#f00" />
    <item
        custom:state_error="false"
        android:color="@android:color/primary_text_dark"/>
    <item android:color="@android:color/primary_text_dark"/>
</selector>

And I add my new stylish thing to the XML inclusion of my custom class:

<com.mypackage.MySpecialEditText
     android:id="@+id/some_id"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:background="@android:color/transparent"
     android:hint="@string/a_nice_hint"
     android:inputType="number"
     android:textColor="@color/red_on_error_dark"
     />

So far, so good. Now, I create my class that extends EditText:

public class MySpecialEditText extends EditText {

    private static final int[] STATE_ERROR = {R.attr.state_error};
    private boolean mStateError;
    //... lots of other fun functionality

    // Time to set my custom status
    public void setStateError(boolean stateError) {
        mStateError = stateError;
        invalidate();
        refreshDrawableState();
    }

    // And now, I need to add the custom state drawable.
    @Override
    protected int[] onCreateDrawableState(int extraSpace) {
        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
        if (mStateError) {    
            mergeDrawableStates(drawableState, STATE_ERROR);
        } 
        return drawableState;
    }
}

All of this is easy enough to put together. However, when I run the code, it always draws the text as red.

Things I have verified:

  1. That I'm correctly setting the error/no-error states.
  2. That the code is going through the onCreateDrawableState method during the non-error times! It skips the mergeDrawableStates method with the extra attribute. (I have also added an else clause and returned a drawableState with no added extra spaces).
  3. Even though the attribute is boolean, and should theoretically be able to accept a value of true or false, I've added a separate attr STATE_VALID, and changed my selector to be as follows.

I have even gone so far as to change the selector to the exhaustive:

<item custom:state_error="true" android:state_selected="true" android:color="#f00" />
<item custom:state_error="true" android:state_focused="true" android:color="#f00" />
<item custom:state_error="true" android:state_pressed="true" android:color="#f00" />

<item custom:state_error="false" android:state_selected="true" android:color="@android:color/primary_text_dark" />
<item custom:state_error="false" android:state_focused="true" android:color="@android:color/primary_text_dark" />
<item custom:state_error="false" android:state_pressed="true" android:color="@android:color/primary_text_dark" />

And I've gone one farther and combined the unnecessary-extra-state with a bunch of android:state_blah, so I have something like 20 different cases in my selector. (Actually, in that case, it always stays white, which isn't much better).

Now, what I'd really, really like is to declare a styleable value that other people (who use my sdk) can set a value to if they so choose. But for now I'll be happy to know why the text is always red.

Edit: One totally legit workaround is simply to call setTextColor when I reach an error state in MySpecialEditText. This is my interim solution, but it prevents someone from using my control and adding a custom:state_error=@color/some_custom_color, or a similar value in theme of their choosing. So long as I'm calling setTextColor by hand, the error color is always my particular shade of red.

I could go further and add a new constructor or setter for the error color, but that's not a very nice solution for a library. If you use the library and instantiate my control, it will pick up all the EditText styles you already have, so it looks native. And if you want to customize the error color, I'd love for you to be able to do that in one place for your whole app with a nice piece of XML that you can run by a designer.

Any ideas? A pre-emptive thank you to all answerers and attempted answerers.

0

There are 0 best solutions below