How to set the color of the cursor(caret) multiple times in Android

228 Views Asked by At

Lets say we have a simple EditText and I want to change the cursor(caret) to some other color, before we were use reflections to get access to the private fields, but with introduction of Android API Q(29), we can now use textCursorDrawable to set the drawable for the blinking cursor.

Here is the xml code of the EditText

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Test"
        android:textSize="30sp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Now we can use a WrapDrawable to wrap a ColorDrawable, that will be set as textCursorDrawable value of the EditText, in order for us to change the cursor color.

Here is the code for the WrapDrawable:

class WrapDrawable(color: Int) : Drawable() {

    private var drawable = ColorDrawable(color)

    @ColorInt
    var color: Int = color
        set(value) {
            field = value
            drawable = ColorDrawable(value)
        }

    override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
        super.setBounds(left, top, right, bottom)
        drawable.setBounds(left, top, right, bottom)
    }

    override fun getConstantState(): ConstantState? {
        return drawable.constantState
    }

    override fun setAlpha(alpha: Int) {
        drawable.alpha = alpha
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        drawable.colorFilter = colorFilter
    }

    override fun getOpacity(): Int {
        return drawable.alpha
    }

    override fun draw(canvas: Canvas) {
        drawable.draw(canvas)
    }

    override fun getIntrinsicWidth(): Int {
        return drawable.bounds.width()
    }

    override fun getIntrinsicHeight(): Int {
        return drawable.bounds.height()
    }
}

In the code below, we change the color of the cursor twice once to Color.RED and second time to Color.BLUE, now we should expect to have a BLUE cursor. But the problem is that once textCursorDrawable is set, we cannot change it even if we try nullify it.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val text = findViewById<EditText>(R.id.editText)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

            // set the cursor color to RED
            text.textCursorDrawable = WrapDrawable(Color.RED).apply {
                setBounds(0, 0, 5, text.lineHeight)
            }
            
            // set the cursor color to BLUE !!! NOT WORKING !!!
            text.textCursorDrawable = WrapDrawable(Color.BLUE).apply {
                setBounds(0, 0, 5, text.lineHeight)
            }
        }
    }
}

enter image description here

So my question is how can we reassign the textCursorDrawable value multiple times?

I have found a workaround by updating the already existing textCursorDrawable value, and changing the ColorDrawable using the color variable.


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val text = findViewById<EditText>(R.id.editText)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

            // set the cursor color to RED
            text.textCursorDrawable = WrapDrawable(Color.RED).apply {
                setBounds(0, 0, 5, text.lineHeight)
            }

            // set the cursor color to BLUE
            text.textCursorDrawable?.let {
                if (it is WrapDrawable) {
                    it.color = Color.BLUE
                    it.setBounds(0, 0, 5, text.lineHeight)
                }
            }
        }
    }
}
2

There are 2 best solutions below

3
Cheticamp On

The documentation for setTextCursorDrawable() states:

Note that any change applied to the cursor Drawable will not be visible until the cursor is hidden and then drawn again.

I have taken a quick look through the TextView and EditText code and haven't determined how to make the change you want. I am not saying that it can't be done; I just don't see it.

Instead, try making a change to your WrapDrawable like this:

(text.textCursorDrawable as WrapDrawable).apply {
    color = Color.BLUE
    setBounds(0, 0, 5, text.lineHeight)
}

This will work and will save the instantiation of a new WrapDrawable.

Update

Can't prove a negative, but it looks like the cursor drawable can't be replaced once set. The following is the reasoning.

For API 31, there are only two places within the TextView code where the cursor drawable is set. The private scope of mCursorDrawable will restrict outside access.

In TextView.java:

private Drawable mCursorDrawable;

public void setTextCursorDrawable(@Nullable Drawable textCursorDrawable) {  
    mCursorDrawable = textCursorDrawable;  
    mCursorDrawableRes = 0;  
    if (mEditor != null) {  
        mEditor.loadCursorDrawable();  
    }  
}

@Nullable public Drawable getTextCursorDrawable() {  
    if (mCursorDrawable == null && mCursorDrawableRes != 0) {  
        mCursorDrawable = mContext.getDrawable(mCursorDrawableRes);  
    }  
    return mCursorDrawable;  
}

It is the text editor class that the cursor is drawn and it reaches back into the TextView to get the drawable that will be used.

In Editor.java:

Drawable mDrawableForCursor = null;

private void drawCursor(Canvas canvas, int cursorOffsetVertical) {  
    final boolean translate = cursorOffsetVertical != 0;  
    if (translate) canvas.translate(0, cursorOffsetVertical);  
    if (mDrawableForCursor != null) {  
        mDrawableForCursor.draw(canvas);  
    }  
    if (translate) canvas.translate(0, -cursorOffsetVertical);  
}

void loadCursorDrawable() {  
    if (mDrawableForCursor == null) {  
        mDrawableForCursor = mTextView.getTextCursorDrawable();  
    }  
}

loadCursorDrawable is the only place that mDrawableForCursor is set so, once it is defined, it can't be changed. Since it can't be changed, it can't be set to null to pick up a new cursor drawable that may be defined in the text view.

So, the long and the short of this is that the cursor can be changed in TextView but cannot be propagated to the editor.

0
Fossor On

You can use LayeredDrawable and colorize its inner Drawable:

public static void setCursorColor(@NonNull EditText editText, int color) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            Drawable drawable = editText.getTextCursorDrawable();
            if (!(editText.getTextCursorDrawable() instanceof LayerDrawable))
                drawable = ContextCompat.getDrawable(editText.getContext(), R.drawable.edit_text_cursor_layer);
            try {
                GradientDrawable colorDrawable = (GradientDrawable) ((LayerDrawable) drawable).findDrawableByLayerId(R.id.cursor);
                colorDrawable.setColor(color);
            } catch (Exception e) {
                e.printStackTrace();
            }
            editText.setTextCursorDrawable(drawable);
    }
}

edit_text_cursor.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <size android:width="2dp" />
</shape>

edit_text_cursor_layer.xml

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/edit_text_cursor" android:id="@+id/cursor"/>
</layer-list>