TextView上のURLリンクで任意の処理を実行する

# 概要
　TextView 上に表示された html のリンクをクリックした際に任意の処理を実行する方法の説明です。

※コードからサクッと概要を把握したい方は [こちら](https://qiita.com/beyondseeker/items/690b5d5293d7c9dddc50#-kotlin-%E3%81%AE-extension-%E7%89%88) を見ていただければ瞬殺だと思われます。

## ◆ 背景
　TextView では [fromHtml](https://developer.android.com/reference/kotlin/androidx/core/text/HtmlCompat#fromhtml) を用いて HTML を表示することができます。

```kotlin
textView.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
```
しかし、リンク押下時に、ブラウザに表示させる以外の方法を用いたい時もあります。

## ◆ 振る舞いを変えたい例
　TextView 上に、以下のような HTML を表示するとします。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/bf1d8b36-32b8-09f7-3f23-356d5905dc65.png">

　Html 上の A タグがクリックされると、href に指定された url がブラウザ等で処理されることになります。しかし、以下のような振る舞いに変更したいことがあります。

- アプリ内の編集画面に遷移させたい。
- 利用規約ページを WebView で開きたい。
- 独自の処理を実行したいがデフォルトの動作(ブラウザを開く等)も同時に実行させたい。

# 要件
　上述のニーズを満たすための要件を、以下のように定義しました。

- html が指定できる。
- リンクの url 文字列に対して、以下のいずれかの処理を指定できる。
    - 任意の処理
    - デフォルトの処理
    - 任意の処理とデフォルトの処理の両方
- Kotlin の extension を用いることにより TextView に対して直接的なコーディングができる。
- Data Binding でも利用できる。

# 実装

## ◆ 呼び出し側コード例

### ☆ kotlin の extension 版
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/bf1d8b36-32b8-09f7-3f23-356d5905dc65.png">

- 編集ページは***こちら*** をタップすると、任意の処理が実行され、デフォルトの処理は握りつぶされる。
- 利用規約は***こちら*** をタップすると、デフォルトの処理(ブラウザ表示や deep linking など)が実行される。

```kotlin
// 表示したい html 文字列
val html = """編集ページは<a href="edit_page">こちら</a>、利用規約は<a href="http://example.com">こちら</a>をご覧ください。"""

// TextView に html 文字列とクリック用のハンドラを渡す
textView.setLinkClickListenable(html) { url ->
    // この url が a タグの href に指定された文字列 
    when (url) {
        "edit_page" -> { showEditPage(); true } // showEditPage() はクリック時に実行させたい任意の処理。(falseを返せばデフォルトの処理も実行される)
        else        -> false // false を返すとデフォルトの処理(http://example.com の表示)が実行される。(trueを返せば握りつぶせる)
    }
}

```

### ☆ Data Binding 版
　Data Binding 化した以外は kotlin の extension 版と同様。

```kotlin
// 表示したい html 文字列
val html = """編集ページは<a href="edit_page">こちら</a>、利用規約は<a href="http://example.com">こちら</a>をご覧ください。"""

// binding
val binding = DataBindingUtil.setContentView<Sample4Sample4bActivityBinding>(this, R.layout.sample4_sample4b_activity)

// binding に html 文字列とクリック用のハンドラを渡す
binding.linkClickListenable = SimpleLinkClickListenable(html){ url ->
    // この url が a タグの href に指定された文字列 
    when (url) {
        "edit_page" -> { showEditPage(); true } // showEditPage() はクリック時に実行させたい任意の処理。(falseを返せばデフォルトの処理も実行される)
        else        -> false // false を返すとデフォルトの処理(http://example.com の表示)が実行される。(trueを返せば握りつぶせる)
    }
}
```

```xml:layout.xml(DataBinding関連行のみを抜粋したもの)
<layout>
    <data>
        <variable
            name="linkClickListenable"
            type="com.objectfanatics.infra.androidx.appcompat.widget.LinkClickListenable" />
    </data>
    <TextView
        app:linkClickListenable="@{linkClickListenable}" />
</layout>
```

## ◆ ライブラリ的コード
```kotlin:AppCompatTextViewExt.kt
package com.objectfanatics.infra.androidx.appcompat.widget

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.view.MotionEvent.ACTION_UP
import android.widget.TextView
import androidx.core.text.HtmlCompat
import androidx.databinding.BindingAdapter

fun TextView.setLinkClickListenable(html: String, onLinkClick: (url: String) -> Boolean) {
    text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT)
    movementMethod = ClickListenableLinkMovementMethod(onLinkClick)
}

class ClickListenableLinkMovementMethod(private val onClick: ((url: String) -> Boolean)) : LinkMovementMethod() {
    override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
        val url = getUrl(widget, buffer, event)
        return when {
            event.action == ACTION_UP && url != null && onClick(url) -> true
            else                                                     -> super.onTouchEvent(widget, buffer, event)
        }
    }

    companion object {
        private fun getUrl(widget: TextView, buffer: Spannable, event: MotionEvent): String? {
            val x = event.x.toInt() - widget.totalPaddingLeft + widget.scrollX
            val y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY
            val off = widget.layout.run { getOffsetForHorizontal(getLineForVertical(y), x.toFloat()) }
            return (buffer.getSpans(off, off, ClickableSpan::class.java).getOrNull(0) as? URLSpan)?.url
        }
    }
}

@BindingAdapter("linkClickListenable")
fun TextView.setLinkClickListenable(linkClickListenable: LinkClickListenable) {
    linkClickListenable.run { setLinkClickListenable(html, onLinkClick) }
}

interface LinkClickListenable {
    val html: String
    val onLinkClick: (url: String) -> Boolean
}

class SimpleLinkClickListenable(
    override val html: String,
    override val onLinkClick: (url: String) -> Boolean
) : LinkClickListenable
```

# おわりに
　これダメじゃんとかこういう機能も欲しいよね的なアイデアなどあればぜひ教えてくださいmm

# 参考
　[HTML to text with custom on link click listener](https://stackoverflow.com/questions/52201205/html-to-text-with-custom-on-link-click-listener)