Community content often has the need to insert links, which leads to the need for linked UI and click interaction, which we often see in the list page and details page in microblog. Let’s analyze the implementation of this function.

Link matching and display interaction

First of all, let’s analyze the components of the link. We can be sure that we need a display title. We may do some processing on the UI performance of this title (common is a link logo and set different colors) to prompt and attract the attention of the user. This link can be internal or external (which is a business requirement). There may be different schemes for link matching. Here we choose the matching mode using a tag, that is, the interface will give us the link data in the form of a tag, and the client will match the data. Here we give a simple example, the interface returns the following data:

<a href="https://www.qq.com"> I am a link </a>11111<a href="https://www.baidu.com"> I am also a link </a>Copy the code

Next, we deal with the data:

/ suspend fun computeLenFilterLink(text: String, mContext: Context): SpannableStringBuilder = withContext(Dispatchers.Default) { var strings = SpannableStringBuilder(text) val pattern = "<a  \s*href\s*=\s*(? :. *?) > (. *?) </a\s*>" val p = Pattern.compile(pattern) val matcher = p.matcher(strings) while (matcher.find()) { val str = matcher.group() val linkTitle = matcher.group(1) ?: "" regular match val / / a tag links patternUrlString =" \ s * (? I) href = \ \ s * s * (" ([^ "] * ") | '[^'] * '| ([^' "> \ s] +))" val patternUrl = Pattern.compile( patternUrlString, Pattern.case_insensitive) // linkUrl val matcherUrL = patternurl.matcher (strings) var linkUrl = "" while (matcherUrL.find()) { linkUrl = matcherUrL.group() linkUrl = linkUrl.replace("href\s*=\s*(['|"]*)".toRegex(), "") linkUrl = linkUrl.replace("['|"]".toRegex(), "") linkUrl = linkUrl. Trim {it <= ' '} break} val sb = SpannableString("#$linkTitle") sb. SetSpan ("#$linkTitle") ForegroundColorSpan( ContextCompat.getColor(mContext, R.color.link_color) ), 0, sb.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE ) sb.setSpan( object : MyLinkClickSpan(mContext) { override fun onSpanClick(widget: MakeText (mContext, "click link =$linkUrl", toast.length_short).show()}}, 0, sb.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE ) val start = strings.indexOf(str) strings.delete(start, // insert strings. Insert (start, sb)} return@withContext strings}Copy the code

Here, we take out the title and jump link of the link in the way of regular matching, and then set the symbol and color of the link to replace the original A label and insert it into the body data. The next step is to set the displayed data:

<a href="https://www.qq.com"> </a>11111<a href="https://www.baidu.com"> Ha ha ha ha "lifecycleScope launch {val contentString = LinkCheckHelper.com puteLenFilterLink (string, this @ MainActivity) tvLink.text = contentString }Copy the code

Click interaction of links

Referring to the interaction effect of weibo, we can find that when we touch the linked content, its background will change from transparent to the color of the linked text, and then back to transparent when we lift our fingers. MyLinkClickSpan = MyLinkClickSpan = MyLinkClickSpan = MyLinkClickSpan = MyLinkClickSpan = MyLinkClickSpan = MyLinkClickSpan

class MyLinkClickSpan(private val context: Context) :
    ClickableSpan(), IPressedSpan {
    private var isPressed = false
    abstract fun onSpanClick(widget: View?)
    override fun onClick(widget: View) {
        if (ViewCompat.isAttachedToWindow(widget)) {
            onSpanClick(widget)
        }
    }

    override fun setPressed(pressed: Boolean) {
        isPressed = pressed
    }

    override fun updateDrawState(ds: TextPaint) {
        if (isPressed) {
            ds.bgColor = ContextCompat.getColor(context, R.color.link_bg_color)
        } else {
            ds.bgColor = ContextCompat.getColor(context, android.R.color.transparent)
        }
        ds.isUnderlineText = false
    }
}
Copy the code

We find that the change of background color needs to be processed separately for finger pressing and lifting, so it is inevitable that we need to deal with touch events at this time:

class LinkTextView(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs) { private var mPressedSpan: IPressedSpan? = null init {isFocusable = false isLongClickable = false MyLinkMovementMethod.instance highlightColor = Color.TRANSPARENT } override fun onTouchEvent(event: MotionEvent): Boolean { val text = text val spannable = Spannable.Factory.getInstance().newSpannable(text) if (event.action == MPressedSpan = getPressedSpan(this, spannable, event) } return if (mPressedSpan ! = null) {/ / if you have go clickSpan MyLinkMovementMethod onTouchEvent MyLinkMovementMethod. The instance. The onTouchEvent (this, getText() as Spannable, event) } else { super.onTouchEvent(event) } } private fun getPressedSpan( textView: TextView, spannable: Spannable, event: MotionEvent ): IPressedSpan? { var mTouchSpan: IPressedSpan? = null var x = event.x.toInt() var y = event.y.toInt() x -= textView.totalPaddingLeft x += textView.scrollX y -= textView.totalPaddingTop y += textView.scrollY val layout = textView.layout val line = layout.getLineForVertical(y) try { var off = layout.getOffsetForHorizontal(line, X.t oFloat ()) if (x < layout. GetLineLeft (line) | | x > layout. GetLineRight (line)) {/ / didn't actually point to anything off = 1} val linkSpans  = spannable.getSpans(off, off, IPressedSpan::class.java) if (! linkSpans.isNullOrEmpty()) { mTouchSpan = linkSpans[0] } return mTouchSpan } catch (e: IndexOutOfBoundsException) { Log.d(this.toString(), "getPressedSpan", e) } return null } }Copy the code
class MyLinkMovementMethod : LinkMovementMethod() {
    override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
        return (sHelper.onTouchEvent(widget, buffer, event))
    }

    companion object {
        val instance: MyLinkMovementMethod
            get() {
                if (sInstance == null) {
                    sInstance = MyLinkMovementMethod()
                }
                return sInstance as MyLinkMovementMethod
            }
        private var sInstance: MyLinkMovementMethod? = null
        private val sHelper = SpanClickHelper()
    }
}
Copy the code
class SpanClickHelper { private var mPressedSpan: IPressedSpan? = null fun onTouchEvent( textView: TextView, spannable: Spannable, event: MotionEvent ): Boolean { return when (event.action) { MotionEvent.ACTION_DOWN -> { mPressedSpan = getPressedSpan(textView, spannable, event) if (mPressedSpan ! = null) {// Set finger press to true and change the corresponding link text background color mPressedSpan!! .setpressed (true) // setSelection. SetSelection (spannable, spannable. GetSpanStart (mPressedSpan), spannable.getSpanEnd(mPressedSpan) ) } mPressedSpan ! = null } MotionEvent.ACTION_MOVE -> { val touchedSpan = getPressedSpan(textView, spannable, event) if (mPressedSpan ! = null && touchedSpan ! = mPressedSpan) {// Set press to false for finger movement and set the corresponding link text background color back to transparent mPressedSpan!! SetPressed (false) mPressedSpan = null / / remove the selected area Selection. RemoveSelection (spannable)} mPressedSpan! = null } MotionEvent.ACTION_UP -> { var touchSpanHint = false if (mPressedSpan ! = null) {touchSpanHint = true // Set finger lift to false, and the corresponding link text background color is set back to transparent mPressedSpan!! .setPressed(false) // Pass the click event callback to mPressedSpan!! .onClick(textView) } mPressedSpan = null Selection.removeSelection(spannable) touchSpanHint } else -> { if (mPressedSpan ! = null) {// All other Settings are set to false and the corresponding link text background color is set back to transparent mPressedSpan!! SetPressed (false)} / / remove the selected area Selection removeSelection (spannable) false}}} / * * * to determine whether a finger click on the link * / private fun getPressedSpan( textView: TextView, spannable: Spannable, event: MotionEvent ): IPressedSpan? { var x = event.x.toInt() var y = event.y.toInt() x -= textView.totalPaddingLeft y -= textView.totalPaddingTop x += textView.scrollX y += textView.scrollY val layout = textView.layout val line = layout.getLineForVertical(y) try { var off = layout.getOffsetForHorizontal(line, X.t oFloat ()) if (x < layout. GetLineLeft (line) | | x > layout. GetLineRight (line)) {/ / didn't actually point to anything off = 1} val link = spannable.getSpans( off, off, IPressedSpan::class.java ) var touchedSpan: IPressedSpan? = null if (link.isNotEmpty()) { touchedSpan = link[0] } return touchedSpan } catch (e: IndexOutOfBoundsException) { Log.d(this.toString(), "getPressedSpan", e) } return null } }Copy the code

The code is simple: determine whether the click area is a link and then change the background color of the link depending on whether the ClickSpan is pressed or not.

Another thing to note is that the following code must be called:

movementMethod = MyLinkMovementMethod.instance
Copy the code

This method sets the click of the link. In addition, different models on the system will have a link to the highlight color we need to call

highlightColor = Color.TRANSPARENT
Copy the code

Let’s cancel it.

This completes the link’s display and click interaction. Specific effects:

Click to get the source code