炊きたてのご飯が食べたい

定時に帰れるっていいね。自宅勤務できるっていいね。子どもと炊きたてのご飯が食べられる。アクトインディでは積極的にエンジニアを募集中です。

(Android)動的に変化する TextView にもっと読むボタンを設置する


アクトインディ Advent Calendar 2016 14日目の記事になります。子どもとおでかけ情報アプリ「いこーよ」は、マップ上で簡単におでかけ先を探せる検索アプリです。記事を見てくれたパパさん、ママさん、ぜひ一度使ってみてください^^

iko-yo.net

今回は TextView の行数によって、もっと読むボタンを表示する機能の紹介をしたいと思います。API を利用してコンテンツを取得する際、長文の場合は 3 行ぐらい表示して「もっと読む」ボタンを置いて、ボタンをタップしたら開閉したい場合があるかと思います。固定されたテキストであれば xml ファイルに maxLines を指定して、ボタンを押したら maxLines に大きな値をセットすれば良いので実装はとても簡単なんですが、API から取得する場合は、 3 行未満のケースも考慮が必要な為、一工夫必要になります。実装したい内容は以下です。サンプルは GitHub で公開しています andridReadMoreSample

f:id:t-namikata:20161212235932g:plain

  1. コンテンツが 3 行を超える場合は、末尾を「...」で省略して、「もっと読む」ボタンを追加する
  2. 「もっと読む」ボタンをタップすると、コンテンツが全文表示されて、「もっと読む」ボタンは消える
  3. コンテンツが 3 行以下の場合は、「もっと読む」ボタンは表示せず「...」の表示もない

1. コンテンツが 3 行を超える場合は、末尾を「...」で省略して、「もっと読む」ボタンを追加する

Android には maxLines に指定した行数を超えると、末尾に「...」を表示する便利なプロパティがあるので、それを xml で指定してあげるだけで、簡単に実装することができます。

  • maxLines で行数を指定
  • maxLines を超えた場合は ellipsize="end" で末尾に ... を表示
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    tools:context="test.readmoresample.MainActivity"

    ... 省略 ...

    <TextView
        android:id="@+id/content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:maxLines="3"
        android:text="@string/long_content" />

    <Button
        android:id="@+id/read_more"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="もっと読む" />

    ... 省略 ...

</LinearLayout>

2. 「もっと読む」ボタンをタップすると、コンテンツが全文表示されて、「もっと読む」ボタンは消える

maxLines に Integer.MAX_VALUE をセットしてあげることで content の全文を表示され ellipsize の ... も表示されなくなります。

class MainActivity : AppCompatActivity() {
    val contentTextView: TextView by bindView(R.id.content)
    val readMoreButton: Button by bindView(R.id.read_more)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        this.setUpContentTextView()

        this.readMoreButton.setOnClickListener {
            this.contentTextView.maxLines = Integer.MAX_VALUE
            this.readMoreButton.visibility = View.GONE
        }
    }
}

3. コンテンツが 3 行以下の場合は、「もっと読む」ボタンは表示せず「...」の表示もない

コンテンツの内容が動的に変更される場合は、コードで maxLines を上手い具合にセットしてあげる必要があり、条件は以下の 3 つに分類されます。

  1. contentTextView.lineCount > 3 である場合は maxLines に 3 をセットして「もっと読む」ボタンを表示する
  2. contentTextView.lineCount <= 3 であり、実際のコンテンツも 3 行以下の場合は「もっと読む」ボタンを表示しない
  3. contentTextView.lineCount <= 3 であるが maxLines = 3 の設定により折りたたまれていて、実際のコンテンツは 3 行以上の場合は「もっと読む」ボタンを表示する
1. contentTextView.lineCount > 3 である場合は maxLines に 3 をセットして「もっと読む」ボタンを表示する

こちらのチェックは簡単です。 this.contentTextView.post の中で lineCount を行わないと onCreate メソッドなどで実行してしまうと View の状態がまだ確定していない為 lineCount は常に 0 が返ってきてしまうので注意しましょう。

private fun setUpContentTextView() {
    val MAX_LINES = 3

    this.contentTextView.post {
        val needTruncate = (contentTextView.lineCount > MAX_LINES)
        if (needTruncate) {
            this.contentTextView.maxLines = MAX_LINES
            this.readMoreButton.visibility = View.VISIBLE
            return@post
        }
    }
}
2. contentTextView.lineCount <= 3 であり、実際のコンテンツも 3 行以下の場合は「もっと読む」ボタンを表示しない
3. contentTextView.lineCount <= 3 であるが maxLines = 3 の設定により折りたたまれていて、実際のコンテンツは 3 行以上の場合は「もっと読む」ボタンを表示する

次に、すでに maxLines = 3 の指定により、コンテンツが折りたたまれているかどうかを判別して、処理を分岐します。

private fun setUpContentTextView() {
    val MAX_LINES = 3

    this.contentTextView.post {
        ... 省略 ...
    }

    if (contentTextView.isTextTruncated()) {
        this.readMoreButton.visibility = View.VISIBLE
    } else {
        this.readMoreButton.visibility = View.GONE
    }
}

今回は TextView に Extension で実装を追加しました。 getEllipsisCount メソッドにより textView が maxLines を超えているかどうかを判別することができます。

fun TextView.isTextTruncated(): Boolean {
    val layout = this.layout ?: return false
    val lines = layout.lineCount
    if (lines < 1) return false
    val ellipsisCount = layout.getEllipsisCount(lines - 1)
    return ellipsisCount > 0
}

これで contentTextView にセットされた文の長さによって、表示の切り替えが行えるようになったので、あとは API で結果を取得して TextView に値を反映したタイミングで this.setUpContentTextView() を呼び出せば contentTextView にセットされた文の長さによって、コンテンツの省略ともっと読むボタンが表示されるようになります。

今回は「長文をセット」「短文をセット」ボタンにより、表示の切り替えサンプルを作ってみたので、最後にそのコードを紹介したいと思います。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    tools:context="test.readmoresample.MainActivity"

    ... 省略 ...

    <TextView
        android:id="@+id/content"

    <Button
        android:id="@+id/read_more"

    ... 省略 ...

    <LinearLayout
        android:layout_marginTop="@dimen/activity_vertical_margin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/long_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="長文をセット" />

        <Button
            android:id="@+id/short_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="短文をセット" />
    </LinearLayout>

</LinearLayout>
class MainActivity : AppCompatActivity() {
    val contentTextView: TextView by bindView(R.id.content)
    val readMoreButton: Button by bindView(R.id.read_more)
    val setLongContentButton: Button by bindView(R.id.long_content)
    val setShortContentButton: Button by bindView(R.id.short_content)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        this.setUpContentTextView()

        this.readMoreButton.setOnClickListener {
            this.contentTextView.maxLines = Integer.MAX_VALUE
            this.readMoreButton.visibility = View.GONE
        }

        this.setLongContentButton.setOnClickListener {
            this.contentTextView.text = this.getString(R.string.long_content)
            this.setUpContentTextView()
        }

        this.setShortContentButton.setOnClickListener {
            this.contentTextView.text = this.getString(R.string.short_content)
            this.setUpContentTextView()
        }
    }

    private fun setUpContentTextView() {
        val MAX_LINES = 3

        this.contentTextView.post {
            val needTruncate = (contentTextView.lineCount > MAX_LINES)
            if (needTruncate) {
                this.contentTextView.maxLines = MAX_LINES
                this.readMoreButton.visibility = View.VISIBLE
                return@post
            }

            if (contentTextView.isTextTruncated()) {
                this.readMoreButton.visibility = View.VISIBLE
            } else {
                this.readMoreButton.visibility = View.GONE
            }
        }
    }
}