Usage
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 {
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())
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
}