EditTextの下線の色をColorFilterで切り替えるときに注意すること


こんにちは。

EditTextの下線の色の変え方、知ってますか?
EditTextの下線の色を変えるには、いくつかの方法がありますが、今回は、コードから指定するときに使うColorFilterの落とし穴と、解決策について紹介します。

ColorFilterを使って下線の色を変える

ColorFilterは、画像などのリソースに対して上から色を塗ることが出来る便利なクラスです。
EditTextの下線部はbackground drawableとして描画されているので、それを取り出して色を塗ってあげることで色を変えることができます。

EditTextの下線の色を変える.
EditText editText = findViewById(R.id.editText);
Drawable background = editText.getBackground();

// ColorFilterを設定する
background.setColorFilter(ContextCompat.getColor(getContext(), R.color.some_color), PorterDuff.Mode.SRC_IN);

これだけで、基本的に下線の色を変えることができます。
また、反対に色を元に戻す場合は、Drawable#clearColorFilter()を呼び出した後に、EditTextの描画を更新してあげることで実現できます。

EditText editText = findViewById(R.id.editText);
Drawable background = editText.getBackground();

// ColorFilterをリセットする
background.clearColorFilter();
editText.refreshDrawableState();

ただ、これだと一つ問題があります。
以下のGIFを見てください。

このように、EditTextのフォーカスが変わった際に、下線部の色が元に戻ってしまうという現象が起きます。
これは、(私が確認した範囲では)API 22よりも低い端末にて起きる現象でした。

原因と解決策

これは、StateListDrawableという、いくつかの状態を持ったDrawableに対して、clickイベントが起きた際に取得されるStateに対するDrawableのみにFilterがかかっていることが原因ではないかと思います。

これを解決するには、StateListDrawableのStateが変更するタイミングを見て、ColorFilterを設定し直さなければいけないのですが、StateListDrawableにはそういったListenerメソッドは用意されていないため、EditText側を継承したクラスを用意することで解決します。

Viewのメソッドには、Stateを更新した際に呼び出されるdrawableStateChangedというものがあるので、それをOverrideすれば上手く解決することが出来ます。

ColorStateEditText.java
package com.litmon.app.customedittext;

import android.content.Context;
import android.graphics.ColorFilter;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.AppCompatEditText;
import android.support.v7.widget.DrawableUtils;
import android.util.AttributeSet;

public class ColorStateEditText extends AppCompatEditText {

    boolean colorFilterChanged;
    ColorFilter colorFilter;

    public ColorStateEditText(Context context) {
        super(context);
    }

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

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

    public void setColorFilter(ColorFilter colorFilter) {
        this.colorFilter = colorFilter;
        // colorfilterをセットしたときにDrawableStateを更新
        refreshDrawableState();
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();

        Drawable background = getBackground();

        // clearColorFilter後のrefreshDrawableStateでループしてしまうため、
        // booleanの値を使ってループしないように条件分岐
        if (background == null || colorFilterChanged) {
            colorFilterChanged = false;
            return;
        }

        if (colorFilter != null) {
            background.setColorFilter(colorFilter);
        } else {
            background.clearColorFilter();
            colorFilterChanged = true;
            refreshDrawableState();
        }
    }
}

※一部簡略化しています

レイアウトなどで使用されているEditTextを、上で作成したColorStateEditTextに置き換えて、ColorFilterを任意のタイミングでセットするようにします。

MainActivity.java
package com.litmon.app.customedittext;

import android.databinding.DataBindingUtil;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.AppCompatDrawableManager;

import com.litmon.app.customedittext.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        binding.setColorFilterButton.setOnClickListener(v -> {
            binding.editText.setColorFilter(AppCompatDrawableManager.getPorterDuffColorFilter(
                    ContextCompat.getColor(this, R.color.orange), PorterDuff.Mode.SRC_IN));
        });

        binding.clearColorFilterButton.setOnClickListener(v -> {
            binding.editText.setColorFilter(null);
        });
    }
}

※DatabindingとRetroLambdaを使用しています

これで、ColorFilterが上手く働いて、色が正常に変わるようになりました。

まとめ

  • ColorFilterをStateListDrawableに使用する場合は、Stateの切り替えに注意
  • View#drawableStateChanged()をOverrideしていい感じにする
  • Databinding, RetroLambda便利!!

以上です。