Upgrade to Pro — share decks privately, control downloads, hide ads and more …

助けてSpannable 〜俺達は雰囲気でテキスト内リンクをつけている〜

Yoshihiro WADA
September 15, 2017

助けてSpannable 〜俺達は雰囲気でテキスト内リンクをつけている〜

shibuya.apk #18でLTをした 助けてSpannable 〜俺達は雰囲気でテキスト内リンクをつけている〜 の資料です

Yoshihiro WADA

September 15, 2017
Tweet

More Decks by Yoshihiro WADA

Other Decks in Technology

Transcript

  1. 1 Yoshihiro Wada  CyberAgent, Inc. / Adtech Studio @e10dokup

    最近 iOS を始めて血反吐を吐いているっぽい
  2. 4 考えられる解答 Linkify.addLink(textView, Linkify.ALL) 等 Html.formHtml(string) TextView に autolink 属性を付ける

    API の返り値が HTML になってるわけじゃないし… リンクにはなるけどクリックイベントを内部で消化してしまう… ALL にしても拾えないものが… (@-mention とか # とか)
  3. 6 テキスト中のリンクをリンクとして表示する ClickableSpan のサブクラスを実装する class LinkSpan( private val url: String,

    private val listener: ((String) -> Unit)? ) : ClickableSpan() { // クリック時にlistenerをコールする override fun onClick(widget: View?) { listener?.invoke(url) } // リンク化される部分のテキストの表示をいじる override fun updateDrawState(ds: TextPaint?) { super.updateDrawState(ds) } }
  4. 7 テキスト中のリンクをリンクとして表示する 表示する文字列を SpannableString 化する fun String.toSpannable( linkListener: ((String) ->

    Unit)? ): SpannableString { val spannableString = SpannableString(this) val urlMatcher = URL_MATCH_PATTERN.matcher(this) while (urlMatcher.find()) { // Spanを生成して val span = LinkSpan(urlMatcher.group(), linkListener) // マッチした文字列にセットする spannableString.setSpan(span, urlMatcher.start(), urlMatcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } return spannableString }
  5. 8 TextView にリンクとして反映させる SpannableString 化した文字列を TextView にセット textView.text = string.toSpannable

    { link -> // 渡されたリンク文字列linkで何かする。Intentさせるとか。 } このままだとリンクにはなったけどクリックしても何も起きない 「Span をクリックした」 と判別する処理が必要 textView.movementMethod = LinkMovementMethod.getIntstance() 大体 LinkMovementMethod で ok
  6. 9 LinkMovementMethod のカスタム 必要なこと インスタンスの提供 companion object { private var

    _instance: MyMovementMethod? = null var instance: MyMovementMethod? = null get() { if (_instance == null) { _instance = MyMovementMethod() } return _instance } } onTouchEvent での onClick 発火制御 タッチされている座標が Span かどうか判別する処理
  7. 10 LinkMovementMethod のカスタム onTouchEvent での onClick 発火制御 ACTION_DOWN/UP/CANCEL で操作 ACTION_DOWN

    で現在のクリック位置から Span を検出 ACTION_UP + 時間内でクリック中 Span の onClick を呼ぶ ACTION_CANCEL でクリック中 Span をなかったことにする
  8. override fun onTouchEvent( widget: TextView, buffer: Spannable, event: MotionEvent ):

    Boolean { val action = event.action // タッチ位置からSpanを取得する、nullならreturn val currentSpan = findSpan(widget, buffer, event.x.toInt(), event.y.toInt()) currentSpan ?: return false when (action) { MotionEvent.ACTION_DOWN -> { keepingSpan = currentSpan // クリック無効化のための遅延処理 invalidationHandler.postDelayed(DELAY_TIME) { keepingSpan = null } return true } MotionEvent.ACTION_UP -> { // クリックが有効かつDOWN-UPまで同じSpanが維持されていたらonClickを呼ぶ if (currentSpan == keepingSpan) { keepingSpan?.onClick(widget) } keepingSpan = null invalidationHandler.removeCallbacksAndMessages(null) return true } MotionEvent.ACTION_CANCEL -> { keepingSpan = null return true } else -> return false } }
  9. private fun findSpan( widget: TextView, buffer: Spannable, x: Int, y:

    Int ): ClickableSpan? { var x = x var y = y // タッチ座標の補正 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()) // spanを含んているか確認して結果を返す val link = buffer.getSpans(off, off, ClickableSpan::class.java) return if (link.isNotEmpty()) { link[0] } else { null } }
  10. 14 まとめ テキスト内リンクをどう扱うか ただリンク飛ばすだけなら Linkify とかで十分 テキスト内リンクだけイベントを拾いたいなら Span だけ実装して LinkMovementMethod

    を実装すれば OK テキスト全体のイベントも扱うなら LinkMovementMethod を 拡張してリンクのイベントと排他的に処理しよう ただここまでやる必要があるのはチャットや SNS のタイムライン くらいでは…? https://github.com/e10dokup/linksample にサンプルあります