みずきち日記

ひらすらプログラミング

EditText 中の任意の文字列に対してハイライト操作を行う

EditText に入力された文字列が存在したとして,任意の区間の文字列に対してハイライト表示したいケースを考えます. Twitterの文字数制限のUIみたいな感じです.

f:id:mzkii:20190224233829p:plain

TextView に対してハイライト処理するサンプルはいくつもありましたが, EditText に関するサンプルが少なく,少しハマったので紹介します.

まずはコードです.

入力可能な文字列の長さが INPUT_LIMIT 以内であれば,SUBMIT ボタンが押下可能になり,

INPUT_LIMIT を超えた場合は,超えた分の文字列をハイライトし,SUBMIT ボタンが押せなくなります.

class MainActivity : AppCompatActivity() {

    private val binding by lazy {
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    }

    private val overLimitColorSpan = BackgroundColorSpan(Color.GRAY)
    private val defaultColorSpan = BackgroundColorSpan(Color.TRANSPARENT)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        with(binding) {
            submit.setOnClickListener { editText.text.clear() }
            editText.addTextChangedListener(object : TextWatcher {
                override fun afterTextChanged(s: Editable?) {
                    if (s == null) return
                    Spannable.Factory.getInstance().newSpannable(editText.text).apply {
                        if (s.length <= INPUT_LIMIT) {
                            removeSpan(overLimitColorSpan)
                            setSpan(defaultColorSpan, 0, s.length, getSpanFlags(defaultColorSpan))
                        } else {
                            removeSpan(defaultColorSpan)
                            setSpan(overLimitColorSpan, INPUT_LIMIT, s.length, getSpanFlags(overLimitColorSpan))
                        }
                    }.also {
                        val selectionStart = editText.selectionStart
                        val selectionEnd = editText.selectionEnd
                        editText.removeTextChangedListener(this)
                        editText.setText(it, TextView.BufferType.SPANNABLE)
                        editText.setSelection(selectionStart, selectionEnd)
                        editText.addTextChangedListener(this)
                    }
                    updateSubmit(editText.text)
                }

                override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
                override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
            })
        }
        updateSubmit("")
    }

    private fun updateSubmit(inputText: CharSequence) {
        with(binding) {
            val textDiff = INPUT_LIMIT - inputText.length
            val canSubmit = textDiff >= 0
            textCount.setTextColor(if (canSubmit) Color.BLACK else Color.RED)
            textCount.text = textDiff.toString()
            submit.isEnabled = canSubmit
        }
    }

    companion object {
        private const val INPUT_LIMIT = 10
    }
}

TextWatcher は,EditText に入力されている(された / あるいはされる前の)文字列の状態を監視するクラスです.今回は 入力後の文字列の状態を把握したいので,afterTextChanged に処理を書いています.

このコードでのポイントは,setText する前に removeTextChangedListener() を呼び出していることです.

TextWatcher クラスメソッド内部で setText を呼び出すと,キーボード入力した時と同様に再び ~Changed() メソッドが呼ばれてしまいループ状態に陥ってしまうので,setText する直前に一旦自分自身の Listener を削除しておき,setText 完了後に再び addTextChangedListener() で自分自身を登録します.

BackgroundColorSpan() は文字列の背景色を変えますが,ForegroundColorSpan() を使えば文字色を変更できたりします.

簡単な サンプルプロジェクト を用意しました. 参考になれば幸いです.