読者です 読者をやめる 読者になる 読者になる

アニメイトラボ開発者ブログ

株式会社アニメイトラボの開発者ブログです


アニメイトラボ開発者ブログ

developer.animatelab.com


Android で一部の文字列をリンク化した TextView を生成するクラスを実装してみました #Kotlin

抱き枕と戯れる人生を謳歌するバトルプログラマー柴田智也 id:bps_tomoya です。

Android アプリケーション開発をしていると、一部の文字列をリンク化したテキストを表示したいという場面があると思います。 私も例に漏れずそういったシーンに遭遇したので、ひょっとするともっと簡単な実装ができるのかもしれませんが、独自のクラスを実装してみました。

このクラスは、

  • こちら(URL A のリンクを設定する)
  • こちら(URL B のリンクを設定する)

のようにリンク化対象文字列が重複してしまった場合にも、それぞれにそれぞれのリンクが設定されることを考慮したクラスになっております。
また基礎的な実装のみに止めているため、メソッドに渡す引数などは状況に合わせて実装する必要があることにご留意ください。

検証環境

  • Android 5.1 (HTC Desire 626)
  • Kotlin 1.0.3

コード(Kotlin)

/**
 * 一部の文字列をリンク化した TextView を生成するクラス。
 * Activity から独立したクラスなので、context を受け取ってインスタンス化されます。
 */
class TextViewWithHyperLinkBuilder(context: Context) {
    private val context: Context by lazy { context }

    /**
     * 文字列の一部をリンク化した TextView を返却する。
     */
    fun getTextViewWithHyperLink(): TextView {
        // 対象文字列。
        val contentText = "サイト1はこちら! サイト2はこちら!"

        // リンク化対象の文字列と URL マップ。
        // キー重複の可能性を受容できるようにキーは整数値を割り当てています。
        // また、この後の処理で順序保証が必要となる観点から TreeMap を利用します。
        val map = TreeMap<Int, Map<String, String>>()
        map.put(0, mapOf("こちら" to "https://www.animatelab.com/"))
        map.put(1, mapOf("こちら" to "http://developer.animatelab.com/"))

        // 文字列の一部をリンク化する処理を呼び出し、返り値を TextView に格納する。
        val spannableString = this.createSpannableString(contentText, map)
        val textView = TextView(this.context)
        textView.text = spannableString
        textView.movementMethod = LinkMovementMethod.getInstance()

        return textView
    }

    /**
     * リンク化するべき文字列の一部の範囲を探索し、タップイベントを設定した文字列を返却する。
     */
    private fun createSpannableString(message: String, map: TreeMap<Int, Map<String, String>>): SpannableString {
        val spannableString = SpannableString(message)
        val prevFindEndHistory = HashMap<String, Int>()

        map.entries.forEach { e ->
            val element = e.value.entries.elementAt(0)
            val pattern = Pattern.compile(element.key)
            // 前回の探索で一致した箇所の end 履歴を取り出します。
            // その値を使って対象文字列を substring したものを pattern.matcher に渡すことで、
            // 前回探索した範囲を探索対象から除外します。
            val prevFindEnd = prevFindEndHistory[element.key] ?: 0
            val matcher = pattern.matcher(message.substring(prevFindEnd))
            var start = 0
            var end = 0

            while (matcher.find()) {
                // 探索一致箇所の start と end は元の対象文字列に対する値としてはズレが発生してしまいます。
                // substring した文字数ぶんだけズレが発生しているのでprevFindEnd を加算します。
                start = matcher.start() + prevFindEnd
                end = matcher.end() + prevFindEnd
                break
            }

            // 今回の探索で一致した箇所の end 履歴を格納します。
            prevFindEndHistory.put(element.key, end)

            // 対象文字列のリンク化するべき範囲にイベントを設定します。
            spannableString.setSpan(object: ClickableSpan() {
                override fun onClick(textView: View) {
                    val url = element.value
                    val uri = Uri.parse(url)
                    val intent = Intent(Intent.ACTION_VIEW, uri)
                    context.startActivity(intent)
                }
            }, start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
        }

        return spannableString
    }
}

生成された TextView を Activity に表示すると、このような結果になります。

f:id:bps_tomoya:20160805133231p:plain:w300

1つめの「こちら」をクリックするとアニメイトラボのサイトが、

f:id:bps_tomoya:20160805132358p:plain:w300

2つめの「こちら」をクリックするとアニメイトラボ開発者ブログが WEB ブラウザで開きます。

f:id:bps_tomoya:20160805132405p:plain:w300

ぜひ参考にしてみてください!