タイトルテキストがMarqueeするToolbarを作る
2020年にもなって文字列をmarqueeさせたくなったので作る
Created at

815 Words
⚠️

やりたいこと

こういうの。

やりたいやつ

ツールバーのtitle部分の文字列が横に流れ続けている。

カスタムビューの作成

Toolbarを継承した次のようなカスタムビューを作成する。

MarqueeToolbar.kt

package com.suihan74.utilities

import android.content.Context
import android.text.TextUtils
import android.util.AttributeSet
import android.widget.TextView
import androidx.appcompat.widget.Toolbar

class MarqueeToolbar : Toolbar {
    constructor(context: Context, attributeSet: AttributeSet? = null) :
            super(context, attributeSet)

    constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int) :
            super(context, attributeSet, defStyleAttr)

    /** タイトルの設定が完了しているか否か */
    private var reflected: Boolean = false
    /** タイトル部分のTextView */
    private var titleTextView: TextView? = null

    override fun setTitle(resId: Int) {
        if (!reflected) {
            reflected = reflectTitle()
        }
        super.setTitle(resId)
        selectTitle()
    }

    override fun setTitle(title: CharSequence?) {
        if (!reflected) {
            reflected = reflectTitle()
        }
        super.setTitle(title)
        selectTitle()
    }

    /** Viewが生成されたときに呼ばれる */
    override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
        super.onWindowFocusChanged(hasWindowFocus)
        if (hasWindowFocus && !reflected) {
            reflected = reflectTitle()
            selectTitle()
        }
    }

    /** タイトル部分のTextViewを設定 */
    private fun reflectTitle() =
        try {
            val field = Toolbar::class.java.getDeclaredField("mTitleTextView").apply {
                isAccessible = true
            }
            titleTextView = (field.get(this) as? TextView)?.apply {
                ellipsize = TextUtils.TruncateAt.MARQUEE
                marqueeRepeatLimit = -1   // forever
            }

            titleTextView != null
        }
        catch (e: Throwable) {
            e.printStackTrace()
            false
        }

    /** タイトル部分を選択 */
    private fun selectTitle() {
        titleTextView?.isSelected = true
    }
}

参考:
A Marquee-able Android Toolbar. | InsanityOnABun / MarqueeToolbar.java | GitHub Gist

説明

要するに、Toolbarのprivateフィールドであるタイトル部分のTextViewをリフレクションで強引にぶっこ抜いてきてmarquee用の設定を追加しているというわけ。(reflectTitle()部分)

なお、onWindowFocusChanged()をオーバーライドしているのは何らかの方法1でViewが生成完了して画面に表示されるまでの間にreflectTitle()が実行されないとsetTitle()が呼ばれるまでアニメーションが始まらないため。
Activity.onCreate()内でtoolbar.title = "長い文字列"を記述してもViewのライフサイクル的な問題でreflectTitle()内のfield.get(this)に失敗する)

使用方法

activity_hoge.xml

<androidx.coordinatorlayout.widget.CoordinatorLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
            android:id="@+id/appbar_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <com.suihan74.utilities.MarqueeToolbar
                android:id="@+id/toolbar"
                app:layout_scrollFlags="enterAlways|scroll"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>

    </com.google.android.material.appbar.AppBarLayout>

    ...

</androidx.coordinatorlayout.widget.CoordinatorLayout>

余談

「サブタイトルもmarqueeさせたいぜ」という欲張りさんは同じようにしてMarqueeToolbarmSubtitleTextViewをぶっこ抜いて設定すればいいんだと思う。


  1. 何らかの方法→ ViewのonStart/onPause - Qiita ↩︎

See Also