This post will be about an interesting observation I made when working on a custom View that did not extend EditText
, but contained one.
To show an example, I created a project that contains a widget called ToggleEditText. This simple widget has the following features:
- it contains an
EditText
and aCheckBox
- toggling the CheckBox enables/disables the EditText
- it overrides
View
‘s setEnabled(boolean) method to achieve the same result as toggling the checkbox
The problem
Starting with the previous example, let’s say that I would like to make the ToggleEditText start with a disabled state. Since the View class contains the setEnabled method, one could think that this is just as simple as adding android:enabled="false"
to our edit text in the layout:
<com.gyulajuhasz.example.enabledattribute.view.ToggleEditText android:id="@+id/main_activity_toggle_edit_text" android:layout_width="match_parent" android:layout_height="wrap_content" android:enabled="false" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" />
After trying it with eagerness, we may get a little surprise: it just does not work. You can download the code from here.
The expected result
To illustrate how this should have worked I created a CustomEditText
that extends AppCompatEditText and added it to the layout with android:enabled="false"
:
<com.gyulajuhasz.example.enabledattribute.view.CustomEditText android:layout_width="match_parent" android:layout_height="wrap_content" android:enabled="false" android:hint="@string/should_be_disabled_by_default" app:layout_constraintBottom_toTopOf="@+id/main_activity_toggle_button" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@+id/main_activity_toggle_edit_text" />
When running the code, You can verify that the custom edit text in the center of the screen is disabled. The main question is… what is the difference between the two cases that makes one of them succeed and the other fail?
The explanation
To find out the cause of the unexpected behavior, two basic concepts have to be revised about custom Views:
- a custom View declares its attributes via a
declare-styleable
tag - the values of the attributes are read and applied by using TypedArrays, typically in the constructor
This means that the attributes and code of the View
class has to be checked for the answer. The latest version of the Android framework’s attributes can be found here, and the code of the View
class can be seen here.
Examining the code of the View class, we can notice that R.styleable.View_enabled
is not referenced anywhere in the class. How is this possible? Is this attribute not processed?
Looking at attrs.xml
and searching for the enabled
attribute, we can finally see that this attribute is not declared for View
, but for TextView
instead!
<declare-styleable name="TextView"> ... <!-- Specifies whether the widget is enabled. The interpretation of the enabled state varies by subclass. For example, a non-enabled EditText prevents the user from editing the contained text, and a non-enabled Button prevents the user from tapping the button. The appearance of enabled and non-enabled widgets may differ, if the drawables referenced from evaluating state_enabled differ. --> <attr name="enabled" format="boolean" /> ... </declare-styleable>
Checking the source code of TextView, we can easily find the code that uses the attribute:
case com.android.internal.R.styleable.TextView_enabled: setEnabled(a.getBoolean(attr, isEnabled())); break;
The workaround
Fortunately, it is not too hard to add ToggleEditText
the ability to use the attribute. First, the attribute has to be declared for it and processed in its constructor(s). The declaration can be seen below. Please note that the type of the attribute is not specified, because the attribute is reused. Specifying its type will result in a compilation failure.
<!-- Attributes for the ToogleEditText widget. --> <declare-styleable name="ToggleEditText"> <!-- Reusing android:enabled attribute. --> <attr name="android:enabled" /> </declare-styleable>
All we have to do is process the value of the attribute this way (notice the android_ part in the name of the styleable attribute):
final TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ToggleEditText, defStyleAttr, 0); setEnabled(typedArray.getBoolean(R.styleable.ToggleEditText_android_enabled, isEnabled())); typedArray.recycle();
The code that has this workaround can be seen here. After building and running the app, it can be seen that the ToggleEditText
is disabled by default, just as we wanted.
Conclusion
In my opinion, the enabled attribute should be declared and processed by the View
class, since the corresponding setEnabled(boolean)
method is there. I don’t know if this was an intentional decision by the Android framework developers or it was just accidentally done. To find out, I issued a bug report for Google about this situation in hopes of getting some explanation or a fix for this issue. Until that happens, the desired behavior can still be achieved by redeclaring and reusing the attribute in every custom View where it is needed. However, I think that this should not be necessary.