TextView 文本可点击和高亮最实用封装

151 阅读2分钟

image.png

Usage

//第一种文本超链接写法,如果是固定链接的可以直接填链接地址,不固定则可以填一个tag
String hrefContent = "我已阅读并同意<a href='event_one'>服务协议</a>、<a href='event_two'>隐私保护政策</a>和<a href='event_three'>第三方 SDK 共享信息情况说明</a>";
new Linker.Builder()
        .content(hrefContent)
        .bold(true) //是否加粗
        .linkColor(ContextCompat.getColor(getContext(), R.color.link_text_color)) //高亮的颜色
        .addOnLinkClickListener(content -> {
            if ("event_one".equals(content)) {
                ARApi.ready.goWebView("www.baidu.com").navigation();
            }
        })
        .textView(tvTest)
        .setLinkMovementMethod(LinkMovementMethod.getInstance())
        .apply();
//第二种文本匹配法
String agree = "我已阅读并同意";
String serviceAgreement = "服务协议";
String privacyProtection = "隐私保护政策";
String sdkProtection = "第三方 SDK 共享信息情况说明";
new Linker.Builder()
        .content(agree + serviceAgreement + "、" + privacyProtection + "和" + sdkProtection)
        .bold(true)
        .links(serviceAgreement, privacyProtection, sdkProtection) //填入可以点击的文案列表
        .linkColor(ContextCompat.getColor(getContext(), R.color.link_text_color))
        .addOnLinkClickListener((content) -> {
            if (serviceAgreement.equals(content)) {
                ARApi.ready.goWebView("www.baidu.com").navigation();
            }
        })
        .textView(tvTest)
        .setLinkMovementMethod(LinkMovementMethod.getInstance())
        .apply();

Source code

object Linker {

    fun parseHtml(text: String, color: Int, bold: Boolean, shouldShowUnderLine: Boolean, linkClickListener: OnLinkClickListener?): Spanned {
        val html = Html.fromHtml(text)
        val spans = html.getSpans(0, text.length, URLSpan::class.java)
        val builder = SpannableStringBuilder(html)
        builder.clearSpans()
        for (span in spans) {
            //点击事件
            builder.setSpan(object : ClickableSpan() {
                override fun onClick(widget: View) {
                    linkClickListener?.onClick(span.url)
                }

                override fun updateDrawState(ds: TextPaint) {
                    if (color != 0) {
                        ds.color = color
                    }
                    ds.isFakeBoldText = bold
                    ds.isUnderlineText = shouldShowUnderLine
                }
            }, html.getSpanStart(span), html.getSpanEnd(span), Spanned.SPAN_INCLUSIVE_INCLUSIVE)
        }
        return builder
    }

    class Builder {

        private var linkMovementMethod: MovementMethod? = null
        private lateinit var textView: TextView
        private lateinit var content: String
        private var links: List<String> = ArrayList()
        private var color: Int = Color.BLACK
        private var shouldShowUnderLine: Boolean = false
        private var linkClickListener: OnLinkClickListener? = null
        private var colorLinks: List<Pair<String, Int>> = ArrayList()
        private var bold = false

        fun textView(textView: TextView): Builder {
            this.textView = textView
            return this
        }

        fun content(content: String): Builder {
            this.content = content
            return this
        }

        fun links(link: String): Builder {
            return links(arrayOf(link).asList())
        }

        fun links(vararg links: String): Builder {
            return links(links.asList())
        }

        fun links(links: List<String>): Builder {
            this.links = links
            return this
        }

        fun colorLinks(links: List<Pair<String, Int>>): Builder {
            this.colorLinks = links
            return this
        }


        fun linkColor(color: Int): Builder {
            this.color = color
            return this
        }

        fun shouldShowUnderLine(shouldShowUnderLine: Boolean): Builder {
            this.shouldShowUnderLine = shouldShowUnderLine
            return this
        }

        fun addOnLinkClickListener(listener: OnLinkClickListener): Builder {
            this.linkClickListener = listener
            return this
        }

        fun setLinkMovementMethod(method: MovementMethod): Builder {
            this.linkMovementMethod = method
            return this
        }

        fun bold(bold: Boolean): Builder {
            this.bold = bold
            return this
        }

        fun apply() {
            if (links.isNullOrEmpty()) {
                applyHrefLink(textView, content, color, bold, shouldShowUnderLine, linkClickListener)
            } else {
                applyLink(textView, content, links, color, shouldShowUnderLine, linkClickListener, linkMovementMethod, colorLinks, bold)
            }
        }
    }

    fun applyHrefLink(textView: TextView?, text: String, color: Int, bold: Boolean, shouldShowUnderLine: Boolean, linkClickListener: OnLinkClickListener?) {
        textView?.text = parseHtml(text, color, bold, shouldShowUnderLine, linkClickListener)
        textView?.movementMethod = LinkMovementMethod.getInstance()
    }

    fun applyLink(
        textView: TextView?,
        content: String,
        links: List<String>?,
        color: Int,
        shouldShowUnderLine: Boolean,
        linkClickListener: OnLinkClickListener?,
        linkMovementMethod: MovementMethod?,
        colorLinks: List<Pair<String, Int>>?,
        bold: Boolean
    ) {
        if (textView == null) {
            return
        }

        if ((links == null || links.isEmpty()) && (colorLinks == null || colorLinks.isEmpty())) {
            textView.text = content
            return
        }

        if (colorLinks != null && colorLinks.isNotEmpty()) {
            applyLinkInternal(textView, content, colorLinks, shouldShowUnderLine, linkClickListener, linkMovementMethod, bold)
            return
        }

        if (links != null && links.isNotEmpty()) {
            var colorAndLinks = arrayListOf<Pair<String, Int>>()
            for (link in links) {
                colorAndLinks.add(Pair(link, color))
            }
            applyLinkInternal(textView, content, colorAndLinks, shouldShowUnderLine, linkClickListener, linkMovementMethod, bold)
        }

    }

    private fun applyLinkInternal(
        textView: TextView, content: String, links: List<Pair<String, Int>>,
        shouldShowUnderLine: Boolean,
        linkClickListener: OnLinkClickListener?,
        linkMovementMethod: MovementMethod?, bold: Boolean
    ) {

        val spannableString = SpannableString(content)

        var pattern: Pattern?
        var matcher: Matcher?
        var clickableSpan: ClickableSpan?


        for (value in links) {
            if (TextUtils.isEmpty(value.first)) {
                continue
            }

            pattern = Pattern.compile(value.first)
            matcher = pattern.matcher(content)
            while (matcher.find()) {
                clickableSpan = object : ClickableSpan() {
                    override fun onClick(widget: View) {
                        linkClickListener?.onClick(value.first)
                    }

                    override fun updateDrawState(ds: TextPaint) {
                        if (value.second != 0) {
                            ds.color = value.second
                        }
                        ds.isFakeBoldText = bold
                        ds.isUnderlineText = shouldShowUnderLine
                    }
                }

                spannableString.setSpan(clickableSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
            }
        }

        textView.text = spannableString

        if (linkMovementMethod != null) {
            textView.movementMethod = linkMovementMethod
        } else {
            textView.movementMethod = TextViewLinkMovementMethod().getInstance()
        }
    }
}
interface OnLinkClickListener {
    /**
     * @param content the content which is clicked
     */
    fun onClick(content: String)
}
class TextViewLinkMovementMethod : 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())

            // 命中字符起始X坐标
            val charStartX = layout.getPrimaryHorizontal(off).toInt()

            // 单个字符宽度
            var singleCharWidth = 0
            if (widget.text.isNotEmpty()) {
                singleCharWidth = widget.paint.measureText(widget.text[0].toString()).toInt()
            }

            if (x <= charStartX + singleCharWidth) {// 命中字符范围内,响应点击
                val links = buffer!!.getSpans(off, off, ClickableSpan::class.java)

                if (links.isNotEmpty()) {
                    if (action == MotionEvent.ACTION_UP) {
                        links[0].onClick(widget)
                    }
                    return true
                }
            } else {// 没有命中,消耗事件不处理
                return true
            }
        }
        return super.onTouchEvent(widget, buffer, event)
    }

    fun getInstance(): TextViewLinkMovementMethod {
        if (sInstance == null) {
            sInstance = TextViewLinkMovementMethod()
        }
        return sInstance as TextViewLinkMovementMethod
    }

    private var sInstance: TextViewLinkMovementMethod? = null
}