自作TwitterクライアントAndroidアプリでハッシュタグをリンク化する
最近、qiitaに寄稿していなかったので久々に寄稿。
Kotlin系の記事です。
やりたかったこと
自作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つ
-
val hashTagRegex = Regex("(?:^|\\s)([##]([^\\s]+))[^\\s]?")
- 以下苦労した箇所
// ここが問題の箇所
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 {}
参考資料
- Kotlinのコーディングが捗る標準ライブラリ | qiita
- Android リアルタイム入力でハッシュタグ形式に文字装飾するTIPS | qiita
- 【kotlin】Matcher.find()を呼ばないとMatcher.start()もMatcher.end()も正常に動作しない | takelab.note