【Android】TextViewに含まれたURLをタップした時の挙動を制御する【Kotlin】

TwitMorseのリンク文字列Android
TwitMorse内のこのリンク文字列をタップした時の挙動を制御したい

自作アプリ「TwitMorse」でクラッシュ対応をしていたら、Android6.0系で下記のようなクラッシュが起きることがわかった。

android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?


TextViewに含まれたURLをタップした時の挙動を制御する

状況としてはtwitter4j経由でツイートを取得し、そのツイート内にURLが含まれていた場合にURLをタップするとクラッシュするというバグだった。

以前にもプロフィール画面で、自己紹介文の中にURLが含まれていて、それをタップした場合にクラッシュするというバグを発見していた。
その時は適切な対応方法がわからずにレアケースという事で放置していたのだが、今回のバグはそうはいかない。

かなりのユーザーに影響があるので頑張って対応してみた。
TextViewに含まれたURLをタップした時の挙動をクラッシュしないようにすることができたので後学のために残しておく。

TwitMorseのリンク文字列

TwitMorse内のこのリンク文字列をタップした時の挙動を制御したい

レイアウトファイル

layout/list_item_tweet.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?android:attr/selectableItemBackground"
    android:descendantFocusability="blocksDescendants"
    android:padding="8dp">

    //省略

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/name"
        android:layout_alignStart="@+id/name"
        android:layout_marginTop="3dp"
        android:autoLink="all"
        android:focusable="true"
        android:focusableInTouchMode="false"
        android:text="本文本文本文本文本文本文本文本文本文本文"
        android:textColor="#ffffff"
        android:textSize="14sp" />

</RelativeLayout>

上記のTextView部分、

android:autoLink="all"

によって、URLや電話番号などの文字列が青色のリンク色に変化する。

問題はこいつをタップしたときの挙動である。

LinkMovementMethodを継承して独自のLinkMovementMethodを実装する

AndroidSDKには、LinkMovementMethodなるクラスが存在していて、そいつを拡張するとどうやら解決できそうだった。

MyLinkMovementMethod.kt

import android.content.Intent
import android.net.Uri
import android.text.Selection
import android.text.Spannable
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.URLSpan
import android.view.MotionEvent
import android.widget.TextView

/**
 * TextView内に配置されたhttp://...やhttps://...リンクを押したときの挙動
 * 使用例:
 * TextView.movementMethod = MyLinkMovementMethod.getInstance()
 */
class MyLinkMovementMethod : LinkMovementMethod() {

    override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
        val action = event.action

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
            var x = event.x.toInt()
            var y = event.y.toInt()

            x -= widget.totalPaddingLeft
            y -= widget.totalPaddingTop

            x += widget.scrollX
            y += widget.scrollY

            val layout = widget.layout
            val line = layout.getLineForVertical(y)
            val off = layout.getOffsetForHorizontal(line, x.toFloat())

            val link = buffer.getSpans(off, off, ClickableSpan::class.java)

            if (link.isNotEmpty()) {
                if (action == MotionEvent.ACTION_UP) {
                    /*
                     * Write your custom logic
                     */
                    if (link[0] is URLSpan) {
                        val url = (link[0] as URLSpan).url
                        val context = widget.context
                        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) //とりあえずは外部ブラウザへ
                        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
                        context.startActivity(intent)
                    }
                } else if (action == MotionEvent.ACTION_DOWN) {
                    Selection.setSelection(buffer,
                            buffer.getSpanStart(link[0]),
                            buffer.getSpanEnd(link[0]))
                }

                return true
            } else {
                Selection.removeSelection(buffer)
            }
        }

        return super.onTouchEvent(widget, buffer, event)
    }

    companion object {
        fun getInstance(): MyLinkMovementMethod {
            return MyLinkMovementMethod()
        }
    }
}

ポイントは下記の部分です。

/*
* Write your custom logic
*/
if (link[0] is URLSpan) {
    val url = (link[0] as URLSpan).url
    val context = widget.context
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) //とりあえずは外部ブラウザへ
    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
    context.startActivity(intent)
}

今回のクラッシュはActivity以外からstartActivity()メソッドを呼び出す事に起因するらしく、

intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK

としてあげることでクラッシュが解決される、とのことだった。(ベスト・プラクティスなのかは不明、誰か教えてください)

なお、上述で紹介した部分をいじると独自のWebViewActivityなどにも遷移させることが可能になる。

独自のLinkMovementMethodの使い方

独自LinkMovementMethodであるMyLinkMovementMethodをセットしてあげるだけで良い。

class TweetAdapter(private val context: Context,
                   private var statusSet: List<Status>?) : RecyclerView.Adapter<TweetAdapter.TweetViewHolder>() {

    override fun onBindViewHolder(holder: TweetViewHolder, position: Int) {
        statusSet?.let {
         // 略  
            holder.text.text = it[position].text // holderのTextView
            holder.text.movementMethod = MyLinkMovementMethod.getInstance() // これだけでいい
            // ...あとは省略
        }
}

TextView.movementMethod = MyLinkMovementMethod.getInstance() としてあげるだけでリンク文字列タップ時の挙動が制御できるようになる。これだけでとりあえず外部ブラウザを開いてリンク先へ遷移するようになった。
ただ、まだChromeのバグなのか、Chrome自体がクラッシュすることがある。
とりあえず自作アプリのクラッシュは解消されたので一旦よしとする。


ちなみにFLAG_ACTIVITY_NEW_TASKは必須に

FLAG_ACTIVITY_NEW_TASK requirement is now enforced

With Android 9, you cannot start an activity from a non-activity context unless you pass the intent flag FLAG_ACTIVITY_NEW_TASK. If you attempt to start an activity without passing this flag, the activity does not start, and the system prints a message to the log.

Note: The flag requirement has always been the intended behavior, and was enforced on versions lower than Android 7.0 (API level 24). A bug in Android 7.0 prevented the flag requirement from being enforced.

どうやらAndroid9系以降、Activity以外からstartActivityをする場合、必須のフラグになるそうです。
これは結構きつい対応になるんではないだろうか・・・。

参考資料

コメント

タイトルとURLをコピーしました