網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
前言
社區(qū)內(nèi)容經(jīng)常會(huì)有插入鏈接的需要,這時(shí)就產(chǎn)生了對(duì)鏈接的UI和點(diǎn)擊交互的需求,我們?cè)谖⒉┲幸步?jīng)常會(huì)在列表頁(yè)面和詳情頁(yè)面看到。下邊我們就此功能分析一下具體實(shí)現(xiàn)。
一、鏈接的匹配和顯示交互
首先我們先分析一下鏈接的組成部分,可以肯定的是需要一個(gè)顯示的標(biāo)題,我們可能會(huì)對(duì)這個(gè)標(biāo)題在UI表現(xiàn)上做些處理(常見的是一個(gè)鏈接的標(biāo)志和設(shè)置不同的顏色)來提示和吸引用戶的注意,另外還需要點(diǎn)擊時(shí)跳轉(zhuǎn)的鏈接,這條鏈接可以是內(nèi)部也可以是外部(這就屬于業(yè)務(wù)的需求)。關(guān)于鏈接的匹配方式可能會(huì)有不同的方案,我們這里選擇了使用a標(biāo)簽的匹配方式,也就是接口會(huì)把鏈接的數(shù)據(jù)以a標(biāo)簽的形式給我們,客戶端來進(jìn)行匹配數(shù)據(jù),下邊我們簡(jiǎn)單的舉一個(gè)例子,接口返回?cái)?shù)據(jù)如下:
"你說的<a >我是鏈接</a>11111<a >我也是鏈接</a>好開心啊,哈哈哈哈"
接下來我們對(duì)數(shù)據(jù)的處理:
/**
* 匹配a標(biāo)簽直接插入鏈接
*/
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) ?: ""
//a標(biāo)簽鏈接正則匹配
val patternUrlString =
"\s*(?i)href\s*=\s*("([^"]*")|'[^']*'|([^'">\s]+))"
val patternUrl = Pattern.compile(
patternUrlString,
Pattern.CASE_INSENSITIVE
)
//鏈接url
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
}
//設(shè)置顏色
val sb = SpannableString("#$linkTitle")
sb.setSpan(
ForegroundColorSpan(
ContextCompat.getColor(mContext, R.color.link_color)
), 0,
sb.length,
Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
sb.setSpan(
object : MyLinkClickSpan(mContext) {
override fun onSpanClick(widget: View?) {
//鏈接跳轉(zhuǎn)
Toast.makeText(mContext, "點(diǎn)擊鏈=$linkUrl", Toast.LENGTH_SHORT).show()
}
},
0, sb.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
val start = strings.indexOf(str)
strings.delete(start, start + str.length)
//插入鏈接
strings.insert(start, sb)
}
return@withContext strings
}
這里我們是用正則匹配的方式取出鏈接的標(biāo)題和跳轉(zhuǎn)鏈接,然后設(shè)置鏈接的標(biāo)志和顏色替換原來的a標(biāo)簽插入正文數(shù)據(jù)中。接下來就是設(shè)置顯示的數(shù)據(jù):
val string = "你說的<a >我是鏈接</a>11111<a >我也是鏈接</a>好開心啊,哈哈哈哈"
lifecycleScope.launch {
val contentString = LinkCheckHelper.computeLenFilterLink(string,this@MainActivity)
tvLink.text = contentString
}
二、鏈接的點(diǎn)擊交互
參考微博的交互效果我們會(huì)發(fā)現(xiàn)當(dāng)我們觸摸鏈接內(nèi)容時(shí)其背景會(huì)由透明變?yōu)殒溄游淖值念伾种柑饡r(shí)又置回透明。細(xì)心的你肯定發(fā)現(xiàn)我們?cè)趧偛沛溄訑?shù)據(jù)的插入時(shí)設(shè)置了MyLinkClickSpan做了對(duì)鏈接點(diǎn)擊的處理,那么背景顏色的改變就要在ClickSpan中的 updateDrawState(ds: TextPaint)方法中進(jìn)行處理,代碼如下:
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
}
}
我們發(fā)現(xiàn)背景顏色的變化是需要對(duì)手指按下和抬起分別進(jìn)行處理,所以這時(shí)不可避免的我們就要對(duì)觸摸事件進(jìn)行處理:
class LinkTextView(context: Context, attrs: AttributeSet?) :
AppCompatTextView(context, attrs) {
private var mPressedSpan: IPressedSpan? = null
init {
isFocusable = false
isLongClickable = false
// 有鏈接點(diǎn)擊需求不設(shè)置則點(diǎn)擊無效
movementMethod = MyLinkMovementMethod.instance
highlightColor = Color.TRANSPARENT
}
override fun onTouchEvent(event: MotionEvent): Boolean {
val text = text
val spannable = Spannable.Factory.getInstance().newSpannable(text)
if (event.action == MotionEvent.ACTION_DOWN) {
//按下時(shí)記下clickSpan
mPressedSpan = getPressedSpan(this, spannable, event)
}
return if (mPressedSpan != null) {
//如果有clickSpan就走M(jìn)yLinkMovementMethod的onTouchEvent
MyLinkMovementMethod.instance.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.toFloat())
if (x < layout.getLineLeft(line) || x > layout.getLineRight(line)) {
// 實(shí)際上沒點(diǎn)到任何內(nèi)容
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
}
}
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()
}
}
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) {
//手指按下 設(shè)置按下為true,修改對(duì)應(yīng)的鏈接文字背景顏色
mPressedSpan!!.setPressed(true)
//設(shè)置選中區(qū)域
Selection.setSelection(
spannable, spannable.getSpanStart(mPressedSpan),
spannable.getSpanEnd(mPressedSpan)
)
}
mPressedSpan != null
}
MotionEvent.ACTION_MOVE -> {
val touchedSpan = getPressedSpan(textView, spannable, event)
if (mPressedSpan != null && touchedSpan != mPressedSpan) {
//手指移動(dòng)時(shí) 設(shè)置按下為false,對(duì)應(yīng)的鏈接文字背景顏色置回透明
mPressedSpan!!.setPressed(false)
mPressedSpan = null
//移除選中區(qū)域
Selection.removeSelection(spannable)
}
mPressedSpan != null
}
MotionEvent.ACTION_UP -> {
var touchSpanHint = false
if (mPressedSpan != null) {
touchSpanHint = true
//手指抬起時(shí) 設(shè)置按下為false,對(duì)應(yīng)的鏈接文字背景顏色置回透明
mPressedSpan!!.setPressed(false)
//傳遞點(diǎn)擊事件回調(diào)
mPressedSpan!!.onClick(textView)
}
mPressedSpan = null
Selection.removeSelection(spannable)
touchSpanHint
}
else -> {
if (mPressedSpan != null) {
//其它收拾 都設(shè)置按下為false,對(duì)應(yīng)的鏈接文字背景顏色置回透明
mPressedSpan!!.setPressed(false)
}
//移除選中區(qū)域
Selection.removeSelection(spannable)
false
}
}
}
/**
* 判斷手指是否點(diǎn)擊在鏈接上
*/
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.toFloat())
if (x < layout.getLineLeft(line) || x > layout.getLineRight(line)) {
// 實(shí)際上沒點(diǎn)到任何內(nèi)容
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
}
}
代碼比較簡(jiǎn)單,就是對(duì)點(diǎn)擊區(qū)域判斷是否為鏈接,然后根據(jù)手勢(shì)的操作分別設(shè)置給ClickSpan是否按下,來改變鏈接的背景顏色。
另外需要注意一點(diǎn)的是必須要調(diào)用下邊的代碼:
movementMethod = MyLinkMovementMethod.instance
此方法設(shè)置鏈接的點(diǎn)擊。 另外,不同的機(jī)型上系統(tǒng)會(huì)有一個(gè)鏈接的高亮顏色,我們需要調(diào)用
highlightColor = Color.TRANSPARENT
來取消掉。
這樣,鏈接的顯示和點(diǎn)擊交互就完成了。 具體效果:
點(diǎn)擊獲取源碼
總結(jié)
原文鏈接:https://juejin.cn/post/7035606854575063053
相關(guān)推薦
- 2022-07-13 redis搭建哨兵集群的實(shí)現(xiàn)步驟_Redis
- 2022-09-01 PgSQL條件語(yǔ)句與循環(huán)語(yǔ)句示例代碼詳解_PostgreSQL
- 2022-11-15 Django?使用VScode?創(chuàng)建工程的詳細(xì)步驟_python
- 2022-03-26 使用C語(yǔ)言如何輸出逆序數(shù)_C 語(yǔ)言
- 2022-06-13 C語(yǔ)言結(jié)構(gòu)體超詳細(xì)講解_C 語(yǔ)言
- 2023-11-26 StringBuffer 和 StringBuilder
- 2024-02-16 SpringBoot 攔截器Intercepto的創(chuàng)建與基本使用
- 2022-11-13 C#實(shí)現(xiàn)定義一個(gè)通用返回值_C#教程
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支