Android
Kotlin

EditTextでプレフィックスを付けたり、カンマを追加したりする

何がしたかったのか?

EditTextに10000と入力があった場合に$10,000みたいな表示にしれっと更新したい

ちょっとした悩み

$10,000,の右にキャレットがあった状態で削除キーを押されたら、どうするのがユーザにとって自然な挙動なのか

考えた

  1. ,はシステム的に勝手に挿入してるんだから、0が削除されて$1,000になる
  2. ,がしれっと削除される($10000になる)
  3. キャレット右端固定
    • そもそも,の右にキャレットがある状態がなくなる
    • ただし、$10,000$15,000に変えるのが若干面倒になる…
  4. 入力中は,の表示を外す(ついでに$も外して、表計算アプリ風)

3と4を実装した

1と2は一晩寝かせた結果、「無いな」と落ち着いた

3の実装

  • 軽い気持ちで始めたら、思いの外やる事が多かった
  • キャレットを右端に固定

    • EditText(AppCompatEditText)を継承したクラスでonSelectionChangedをoverrideして、無理矢理位置を固定してやる
    override fun onSelectionChanged(selStart: Int, selEnd: Int) = setSelection(text.length)
    
    • 実際はもうちょっと拡張性を持たせたので、こんな感じ
    override fun onSelectionChanged(selStart: Int, selEnd: Int) {
        when (cursorPosition) {
            1 -> setSelection(text.length)
            2 -> setSelection(0)
            else -> {
                // nothing
            }
        }
    }
    
    • 範囲選択系の機能を止める
      • ここが難儀した
      • setCustomSelectionActionModeCallbackにセットするactionModeCallbackで綺麗に止めれるかと思いきや、そうはいかず、メニュー表示は止めれるが、選択状態には突入してしまう(がキャレットは右端固定なので機能しない)
      • longClickablefalseにすればいけるかと思いきや、効果なし
      • cursorVisiblefalseにする事で選択状態に突入しなくなった
        • キャレットが表示されないことに違和感がありつつも、下線の色でアクティブな事がわかるから許容範囲かな…
    isCursorVisible = false
    customSelectionActionModeCallback = object : ActionMode.Callback {
        override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
            return false
        }
    
        override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
            return false
        }
    
        override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
             return false
        }
    
        override fun onDestroyActionMode(mode: ActionMode?) {
            // nothing
        }
    }
    
    • というわけで、EditText(AppCompatEditText)を継承したクラスの全容はこちら
    PinnedCursorPositionEditText.kt
    package xyz.mayahiro.playwithedittext
    
    import android.content.Context
    import android.support.v7.widget.AppCompatEditText
    import android.util.AttributeSet
    import android.view.ActionMode
    import android.view.Menu
    import android.view.MenuItem
    
    class PinnedCursorPositionEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatEditText(context, attrs, defStyleAttr) {
        private val cursorPosition: Int
    
        init {
            val a = context.theme.obtainStyledAttributes(attrs, R.styleable.PinnedCursorPositionEditText, 0, 0)
    
            try {
                cursorPosition = a.getInt(R.styleable.PinnedCursorPositionEditText_cursorPosition, 0)
            } finally {
                a.recycle()
            }
    
            isCursorVisible = false
            customSelectionActionModeCallback = object : ActionMode.Callback {
                override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
                    return false
                }
    
                override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
                    return false
                }
    
                override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
                    return false
                }
    
                override fun onDestroyActionMode(mode: ActionMode?) {
                    // nothing
                }
            }
        }
    
        override fun onSelectionChanged(selStart: Int, selEnd: Int) {
            when (cursorPosition) {
                1 -> setSelection(text.length)
                2 -> setSelection(0)
                else -> {
                    // nothing
                }
            }
        }
    }
    
  • 入力をしれっと更新

    • TextWatcherafterTextChangedで更新(無限ループに注意しつつ)
    editText.addTextChangedListener(object : TextWatcher {
        private var isEditing: Boolean = false
        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
            // nothing
        }
    
        override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
            // nothing
        }
    
        override fun afterTextChanged(p0: Editable?) {
            if (isEditing) {
                return
            }
    
            isEditing = true
            p0?.let {
                editText.setText(プレフィックス付けたり、カンマ追加したりしたやつ)
            }
            isEditing = false
        }
    })
    
  • 出来上がり

4の実装

  • フォーカスの変更を契機にEditTextのtextを更新する
editText.setOnFocusChangeListener { view, b ->
    if (b) {
        (view as EditText).setText(数字だけにしたやつ)
        view.setSelection(view.text.length)
    } else {
        (view as EditText).setText(プレフィックス付けたり、カンマ追加したりしたやつ)
    }
}

3と4を実装してみて

  • 3の、入力に対して即座に反応があるのは、心地よい気がする
  • 特にフォーカスが外れるタイミングがダイアログを閉じるタイミングしかなかったりしたら、4は無力
  • とはいえ、Excelなんかをよく使ってる人にとっては4も十分自然な挙動だと思うし、捨てがたい
  • デザイナーさんに相談だ!

実装はこちら

https://github.com/mayahiro/PlayWithEditText