Jetpackにおける数入力用のセパレータを追加する



導入
ほとんどの金融アプリでは、数値テキストフィールドが重要な役割を果たしている.人々は正確にこれらの入力ボックスで彼らの命令を登録したいです、そして、一貫した経験は重要です.これらのフィールドの基本的な必需品のうちの1つは、ユーザーが入力している間、数の間で挿入される数千のセパレータです.主な課題は、通常、その正しい位置にカーソルを維持し、迷惑カーソルカーソルを防ぐことです.
Jetpack構成のVisualTransformation Sに加えられた新しい TextField オプションのおかげで、この仕事はEditTextを使用している従来のアプローチより簡単になりました.このポストでは、私のカスタム242479142は、CryptoCurrencyトレーダーアプリで通貨のために意味されているJetpack Composeで提供される新しい施設に移行する結果を説明しました.
一言で言えば、新しいEditTextで何が起こるかは、それがユーザーによって値入力を維持するということです.その結果、TextFieldは2つの部分から構成されなければなりません.
  • 実際の値を、表示される予定のフォーマットに変換します.
  • は、変換されたテキストおよびその逆の原作のテキストの各々のポジションを翻訳する.この場合、コンポーネントはカーソルを右の位置に置くことができます.
  • これらのジョブの両方はVisualTransformationメソッドで行われるべきであり、戻り値は変換されたテキストとオフセットマッピングからなるタイプfilterである.
    例のセットはofficial documentで見つかります、そして、クレジットカード変換の大きいセットはここにあります.



    AntiSandSeparorVisualTransformation変換
    千の区切り記号を実装する方法を見ましょう.初めに、実装が金融アプリケーションで働くことになっていたので、私はそれを言いたいです.

    セパレータの追加

    正規化
    些細なアプローチはテキストを逆の順序で横断して、3つのキャラクタの後にコンマを加えることです.しかし、私はあまりにも多くのregexのように、私はあまりにも何千ものセパレータに適した位置を選択するこのパターンを使用します.
    \B(?=(?:\d{3})+(?!\d))
    
    regex101の助けを借りてこの簡単なパターンを説明しましょう.
  • TransformedTextは3桁を意味します.私はそれらをキャプチャすることに興味がありません(理由を見つけるでしょう).したがって、\d{3}は3の多数である数で数字で作られた文字のグループと一致します.
  • (?:\d{3})は負の先読みです.これは、テキストやドットの末尾のような数ではないアンカーポイントに一致します.前のグループとともに、それは数の多数に等しい長さで数のストリングを意味して、数が終わるところで終わります.それで、それは数の中央であらゆるトリプルを捕えません.
  • 最終的に、肯定的な先見は、パターンが真である位置にマッチする.私たちの場合では、場所は千の区切りを置く.
  • (?:\d{3})+は、最後の場所が含まれていないことを保証します.番号の開始時にセパレータを必要としません.

  • 整数部分の変換
    パターンで、残りの仕事は全く簡単です.コンマを入れるために、文字列に(?!\d)メソッドを使用します.
    override fun filter(text: AnnotatedString): TransformedText {
        val commaReplacementPattern = Regex("""\B(?=(?:\d{3})+(?!\d))""")
        val transformed = 
            text.text.replace(commaReplacementPattern, ",")
        // ...
    }
    
    しかし、さらなる考慮が必要である.ローカライズはあなたのアプリケーションの重要な部分の一つである場合、それは多言語になるだろう.したがって、ハードコード化コンマを直接置く代わりに、\Bを使用します.
    val symbols = DecimalFormat().decimalFormatSymbols
    val comma = symbols.groupingSeparator
    

    なぜreplaceを使用しない?
    このクラスが書式化の前に構文解析を必要とするという事実に加えて、うまく動作しない場合もあります.例えば、ユーザーはいくつかのゼロを加えて、それから左に番号を入れたいかもしれません.その場合、フォーマッタはすべての意味のないゼロをクリアします​便利.

    小数点以下
    前述のように、多くの通貨では、分数の部分が利用可能です.また、この部分が小数点以下の場所を持つことを知っています.それで、我々のトランスは2つのパラメタ、DecimalFormatSymbolsDecimalFormat(強制的な小数点のために)をとります.それはコードのこの部分で終わります.
    val zero = symbols.zeroDigit
    val minFractionDigits = min(this.maxFractionDigits, this.minFractionDigits)
    fracPart = fracPart.take(maxFractionDigits).padEnd(minFractionDigits, zero)
    
    また、入力が端数部分のみである場合、エッジケースが存在するので、ユーザはドットで開始する.この場合、私たちの戦略は読みやすくするためにテキストの開始時にゼロを置くことです.
    //Ensure there is at least one zero for integer places
    val normalizedIntPart =
       if (intPart.isEmpty() && fracPart != null) zero.toString() else intPart
    

    一緒に
    次のコードを終了します.
    /* As android uses icu in the recent versions we just let DecimalFormat to
    ​* take care of the selection
    ​*/
    private val symbols = DecimalFormat().decimalFormatSymbols
    
    private val commaReplacementPattern = Regex("""\B(?=(?:\d{3})+(?!\d))""")
    
    
    override fun filter(text: AnnotatedString): TransformedText {
       val comma = symbols.groupingSeparator
       val dot = symbols.decimalSeparator
       val zero = symbols.zeroDigit
    
       var (intPart, fracPart) = text.text.split(dot)
           .let { Pair(it[0], it.getOrNull(1)) }
    
       //Ensure there is at least one zero for integer places
       val normalizedIntPart =
           if (intPart.isEmpty() && fracPart != null) zero.toString() else intPart
    
       val integersWithComma = normalizedIntPart.replace(commaReplacementPattern, comma.toString())
    
       val minFractionDigits = min(this.maxFractionDigits, this.minFractionDigits)
       if (minFractionDigits > 0 || !fracPart.isNullOrEmpty()) {
           if (fracPart == null)
               fracPart = ""
    
           fracPart = fracPart.take(maxFractionDigits).padEnd(minFractionDigits, zero)
       }
    
       val newText = AnnotatedString(
           integersWithComma + if (fracPart == null) "" else ".$fracPart",
           text.spanStyles,
           text.paragraphStyles
       )
      // ...
    }
    

    オフセットの翻訳
    変換が行われた後、上で説明した2つのオフセットマッピング機能を実装する必要があります.ここでの主なアイデアは正しく位置の前に追加された文字を数えることです.私は、彼らが非常に率直であるので、詳細に進んでいません、そして、私は下記に与えられるコードに十分です.
    private inner class ThousandSeparatorOffsetMapping(
       val originalIntegerLength: Int,
       val transformedIntegersLength: Int,
       val transformedLength: Int,
       val commaIndices: Sequence<Int>,
       addedLeadingZero: Boolean
    ) : OffsetMapping {
       val commaCount = calcCommaCount(originalIntegerLength)
    
       val leadingZeroOffset = if (addedLeadingZero) 1 else 0
    
       override fun originalToTransformed(offset: Int): Int =
           // Adding number of commas behind the character
           if (offset >= originalIntegerLength)
               if (offset - originalIntegerLength > maxFractionDigits)
                   transformedLength
               else
                   offset + commaCount + leadingZeroOffset
           else
               offset + (commaCount - calcCommaCount(originalIntegerLength - offset))
    
       override fun transformedToOriginal(offset: Int): Int =
           // Subtracting number of commas behind the character
           if (offset >= transformedIntegersLength)
               min(offset - commaCount, transformedLength) - leadingZeroOffset
           else
               offset - commaIndices.takeWhile { it <= offset }.count()
    
       private fun calcCommaCount(intDigitCount: Int) =
           max((intDigitCount - 1) / 3, 0)
    }
    

    最終結果

    また、完全な実装をチェックアウトすることができます.
    < div >