[Android]TextViewのEllipsisをカスタムする

はじめに

Androidのアプリ開発ではTextViewを使っている際に1行以上になったり3行以上になったら省略するといった場面があると思います。
その際に TextView には省略するための処理が用意されていますが、省略記号として使えるのは ... のみです。

ある情報に紐づいている数を一文で表示したいと思った時に 情報A + 総件数 で一つのテキストとして表示しようとすると、情報Aの文字列が長い時に 総件数 が省略されて紐づいている情報が何個あるのかわからなくなります。

情報Aは省略されてもいいけど、紐づいてる数は表示したい場合に省略記号をカスタマイズしたいと思いました。
その時の記録を残しておきたいと思います。

実装については、この辺りをそのまま参考にするとできました。
しかし、自分の環境ではうまく動かないことがあったので、忘れないように書き残しておきたいと思います。

前提

  • androidのdatabindingを使用した実装を想定しています。そのため、databindingを使用しない実装方法では、ビルドが通りません。

  • TextViewを継承したカスタムビューを使用して実装を行いますが、カスタムビューの作成方法については記述しません。

実装

TextViewを継承したカスタムビューを作成します。

class CustomEllipsizeTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0)
    : AppCompatTextView(context, attrs, defStyleAttr)

onLayoutが呼ばれるタイミングで省略記号をカスタムする処理を呼び出します。

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
    setLabelAfterEllipsis()
}

テキストが省略されていない時に、省略記号が表示されないようにしておきます。

// 文章が省略されているか確認する
if (layout.getEllipsisCount(maxLines - 1) == 0) {
    // 省略されていなければ何もしない
    return
}

TextViewに設定されているテキストの長さと画面幅を取得しておきます。
参考では省略記号がついた状態の文字列の長さを計算して横幅を取得していましたが、手元の環境では上手くいかないことがあったため、自分自身の幅を取得するようにしました。

val start = layout.getLineStart(0)
val end = layout.getLineEnd(lineCount - 1)
val displayedText = text.toString().substring(start, end)
val displayedWidth = width

カスタムした省略記号とTextViewに設定した文字列を組み合わせた文字列の長さが画面に収まるまで、文字を1文字づつ削っていく。

val ellipsis = "..."
val suffixText = ellipsis + leaveText

var baseText = displayedText
var textWidth = paint.measureText(baseText + suffixText)

while (textWidth > displayedWidth) {
    baseText = baseText.substring(0, baseText.length - 1).trim()
    textWidth = paint.measureText(baseText + suffixText)
    }
val newText = baseText + suffixText
text = newText

コード全体としては以下のようになりました。

class CustomEllipsizeTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0) 
    : AppCompatTextView(context, attrs, defStyleAttr) {
    var leaveText = ""
        set(value) {
            field = value
            requestLayout()
        }

    init {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomEllipsizeTextView, 0, 0)
        mode = typedArray.getInt(R.styleable.CustomEllipsizeTextView_mode, 0)
        typedArray.recycle()
    }

    private fun setLabelAfterEllipsis() {
        // 文章が省略されているか確認する
        if (layout.getEllipsisCount(maxLines - 1) == 0) {
            // 省略されていなければ何もしない
            return
        }

        val start = layout.getLineStart(0)
        val end = layout.getLineEnd(lineCount - 1)
        val displayedText = text.toString().substring(start, end)
        // 画面に表示できるwidthを取得する
        val displayedWidth = width

        // カスタムした省略記号を作成する
        val ellipsis = "..."
        val suffixText = ellipsis + leaveText

        var baseText = displayedText
        // 表示する文字列 + カスタム省略記号の横幅を取得する
        var textWidth = paint.measureText(baseText + suffixText)

        // 自身のview幅より、文字列の長さが小さくなるまで文字を削っていく
        while (textWidth > displayedWidth) {
            baseText = baseText.substring(0, baseText.length - 1).trim()
            textWidth = paint.measureText(baseText + suffixText)
        }
        // 文字列の長さが画面に収まるようになったら、textにセットする
        val newText = baseText + suffixText
        text = newText
    }

    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        super.onLayout(changed, left, top, right, bottom)
        setLabelAfterEllipsis()
    }
}

xml上で省略記号の後ろにつけたい文字列をapp:leaveTextの箇所で設定して使います。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewModel"
            type="com.example.shou.ellipetest.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <com.example.shou.ellipetest.CustomEllipsizeTextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginEnd="20dp"
            android:layout_marginStart="20dp"
            android:singleLine="true"
            android:ellipsize="end"
            android:text="@{viewModel.text}"
            app:leaveText="@{viewModel.leaveText}"
            android:textSize="20sp" />

        </LinearLayout>
</layout>

おわりに

省略記号のカスタムは、省略記号が設定されている値を変更すればできるのではないか程度に当初は考えていました。
実装してみると画面幅や文字列の長さを計算することになり、想定と比べると複雑になりましたが、画面幅を計測して実装する経験を積むことができたので、良かったと思っています。

弊社では、美容に関するすべてのモノ/コト/ヒト/場所を繋げるビューティプラットフォームを実現するため、今後もアプリに力を入れてきます!
そのために必要な最新技術は積極的に取り入れていきます!

弊社ではAndroid/iOSエンジニアを超募集しております。
興味のある方は是非ご連絡ください!

株式会社アイスタイル 中途採用
新規ネイティブアプリやグローバル向けアプリ開発がしたいiOS/Androidエンジニア募集!

Androidエンジニア、新卒入社2年目 料理が好きです。最近はテストに興味があります。