3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【kotlin】自作TwitterクライアントAndroidアプリでハッシュタグをリンク化する

Posted at

自作TwitterクライアントAndroidアプリでハッシュタグをリンク化する

最近、qiitaに寄稿していなかったので久々に寄稿。
Kotlin系の記事です。

やりたかったこと

https://wandering-engineer.tech/wp-content/uploads/Screenshot_1580788577.png

自作Twitterクライアントアプリ「TwitMorse 〜モールス信号でつぶやこう、あなたのSOSに誰かが答えてくれる〜」のAndroid版での挙動で、ハッシュタグに反応するLinkMovementMethodを実装して、ハッシュタグ検索結果画面へ遷移させよう、というものです。

  • ハッシュタグの色付け
  • ハッシュタグをタップしたらハッシュタグ専用画面への遷移

この時、kotlinのMatcherの挙動で苦労したので、後学のために残します。

修正前の実装(クラッシュ発生します)

以下、修正前のコード


        /**
         * ハッシュタグを抽出してタップできるようにする
         * タップするとHashTagSearchResultActivityに遷移する
         * いずれStringUtilなどのUtilクラスにまとめられるようにtextViewも渡す。
         */
        fun onTapHashTag(text: String, textView: TextView) {
            textView.movementMethod = LinkMovementMethod.getInstance() //ClickableSpan#onClickを動かすのに必要
            val spannableString = SpannableStringBuilder(text)
            val hashTagRegex = Regex("(?:^|\\s)([##]([^\\s]+))[^\\s]?")
            val matcher = Pattern.compile(hashTagRegex.toString()).matcher(text)
            // 参考 : https://qiita.com/droibit/items/75416c0955b797931bb8#kotlintext
            hashTagRegex.findAll(text)
                    .map { it.value }
                    .forEach {
                        val clickableSpan = object : ClickableSpan() {

                            override fun onClick(widget: View) {
                                val hashTagSearchResultActivity = Intent(widget.context, HashTagSearchResultActivity::class.java)
                                hashTagSearchResultActivity.putExtra(IntentKeyUtil.HASH_TAG_SEARCH_QUERY, it)
                                hashTagSearchResultActivity.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                                widget.context.startActivity(hashTagSearchResultActivity)
                            }

                            override fun updateDrawState(ds: TextPaint) {
                                super.updateDrawState(ds)
                                ds.isUnderlineText = false
                            }
                        }
                        // ここが問題の箇所
                        spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
                        textView.text = spannableString
                    }
        }

ちょいと長めのコードで申し訳ないですが、
ポイントは2つ


// ここが問題の箇所
spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannableString

Matcher.find()を呼ばないとMatcher.start()もMatcher.end()も正常に動作しない

ハッシュタグの色付けには成功したものの、そのハッシュタグをタップすると下記例外でクラッシュします。
この修正に1日半かけて悩みました・・・。

java.lang.IllegalStateException: No successful match so far
修正前

// ここが問題の箇所
spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannableString

そんなMatchはおきねぇよ!!と。

そこで下記のように修正したら、正常に動作しました。

修正後
if (matcher.find()) {
    spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    textView.text = spannableString
}

上記を見れば分かるようにmatcher.find()を呼んであげるとクラッシュがなくなりました。

修正後の実装

        /**
         * ハッシュタグを抽出してタップできるようにする
         * タップするとHashTagSearchResultActivityに遷移する
         * いずれStringUtilなどのUtilクラスにまとめられるようにtextViewも渡す。
         */
        fun onTapHashTag(text: String, textView: TextView) {
            textView.movementMethod = LinkMovementMethod.getInstance() //ClickableSpan#onClickを動かすのに必要
            val spannableString = SpannableStringBuilder(text)
            val hashTagRegex = Regex("(?:^|\\s)([##]([^\\s]+))[^\\s]?")
            val matcher = Pattern.compile(hashTagRegex.toString()).matcher(text)
            // 参考 : https://qiita.com/droibit/items/75416c0955b797931bb8#kotlintext
            hashTagRegex.findAll(text)
                    .map { it.value }
                    .forEach {
                        val clickableSpan = object : ClickableSpan() {

                            override fun onClick(widget: View) {
                                val hashTagSearchResultActivity = Intent(widget.context, HashTagSearchResultActivity::class.java)
                                hashTagSearchResultActivity.putExtra(IntentKeyUtil.HASH_TAG_SEARCH_QUERY, it)
                                hashTagSearchResultActivity.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                                widget.context.startActivity(hashTagSearchResultActivity)
                            }

                            override fun updateDrawState(ds: TextPaint) {
                                super.updateDrawState(ds)
                                ds.isUnderlineText = false
                            }
                        }
                        if (matcher.find()) {
                            spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
                            textView.text = spannableString
                        }
                    }
        }

これでmatcherの参照が動くためか、java.lang.IllegalStateException: No successful match so farのクラッシュは発生しなくなりまして、思い通りの挙動をしてくれるようになりました。

複数ハッシュタグのリンク化

一つのTwitter投稿にハッシュタグが一つなわけがありません。下記やっておかないと、ひとつめのハッシュタグにしか反応しなくなるので、注意点として一応あげておきます


hashTagRegex.findAll(text).map { it.value }.forEach {}

参考資料

宣伝

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?