目錄
一、場景
二、方案的實作
1、”常規”方案
2、"優化"的處理方案
3、最終方案
三、完整代碼
一、場景
我們知道通常在串列頁面會有很多內容,而且每條內容可能會很長,如果每條內容都全部顯示用戶體驗就很不好,所以,我們通常的處理方案是限制每條內容的行數,這個時候如果想更加明顯的提示用戶該條內容有更多的內容,可以進入詳情頁查看時會在內容最后加上“全文”之類的字眼,尤其是社區內的APP里經常會看到這樣的場景,比如:微博,
二、方案的實作
那如果我們想限制最大行數且在最后顯示...全文該怎么實作呢?我們知道我們通常設定TextView的最大行數是設定maxLines屬性,并設定android:ellipsize="end"表示在內容最后顯示...,但是類似"全文“這樣的文字怎么顯示呢?我想這時大家肯定會想到:在內容最后拼上去啊!沒錯,是需要拼上去,那要怎么拼?怎么拼上去正好在內容的最后,既不提前、又完整顯示”全文“?
1、”常規”方案
網上大多關于這個需求的實作方案都是在textView.setText()之后呼叫textView.post方法,偽代碼:
textView.post(new Runnable() {
@Override
public void run() {
//進行內容的截取和拼接
}
});
或者是設定addOnGlobalLayoutListener監聽,偽代碼:
textView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//進行內容的截取和拼接
}
}
});
其本質和核心都是為了獲取內容的行數,來判斷是否大于我們想設定的最大行數,來進行內容的截取和”全文“的拼接,
但是該方案是在setText()之后進行的截取,也就是TextView已經顯示了內容然后再進行內容的處理再次setText(),那么會有以下明顯的缺點:
1:在性能差的設備上會有閃現全部內容然后再顯示處理后的內容,
2:這樣做會有兩次的setText()操作,在內容很多的串列頁會加大性能的損耗,
2、"優化"的處理方案
這個時候可能有人會說既然繪制完成后再處理會有問題,提前獲取到textView的行數進行處理不就好了嗎?沒錯,我們可以設定addOnPreDrawListener監聽提前獲取行數來進行處理,偽代碼:
textView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw() {
//進行內容的截取和拼接
return false
}
});
但是這種方案只適合單獨一條內容,不適合在串列中使用,因為這樣只有在第一屏有效,且滑動多屏后回到第一屏也會重置為原始資料,
3、最終方案
既然設定addOnPreDrawListener監聽提前獲取行數來進行處理的方案在串列中不可行還有沒有其他方法呢?那當然是在TextView的onMeasure()中測量textView的高度時進行內容的處理,并設定相對應的高度了,這樣就可以保證性能問題又能保證串列中的每條內容都能得到處理,先上代碼:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (lineCount > maxLine) {
//如果大于設定的最大行數
val (layout, stringBuilder, sb) = clipContent()
stringBuilder.append(sb)
setMeasuredDimension(measuredWidth, getDesiredHeight(layout))
text = stringBuilder
}
}
/**
* 裁剪內容
*/
private fun clipContent(): Triple<Layout, SpannableStringBuilder, SpannableString> {
var offset = 1
val layout = layout
val staticLayout = StaticLayout(
text,
layout.paint,
layout.width,
Layout.Alignment.ALIGN_NORMAL,
layout.spacingMultiplier,
layout.spacingAdd,
false
)
val indexEnd = staticLayout.getLineEnd(maxLine - 1)
val tempText = text.subSequence(0, indexEnd)
var offsetWidth =
layout.paint.measureText(tempText[indexEnd - 1].toString()).toInt()
val moreWidth =
ceil(layout.paint.measureText(moreText).toDouble()).toInt()
//表情位元組個數
var countEmoji = 0
while (indexEnd > offset && offsetWidth <= moreWidth ) {
//當前位元組是否位表情
val isEmoji = PublicMethod.isEmojiCharacter(tempText[indexEnd - offset])
if (isEmoji){
countEmoji += 1
}
offset++
val pair = getOffsetWidth(
indexEnd,
offset,
tempText,
countEmoji,
offsetWidth,
layout,
moreWidth
)
offset = pair.first
offsetWidth = pair.second
}
val ssbShrink = tempText.subSequence(0, indexEnd - offset)
val stringBuilder = SpannableStringBuilder(ssbShrink)
val sb = SpannableString(moreText)
sb.setSpan(
ForegroundColorSpan(moreTextColor), 3, sb.length,
Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
//設定字體大小
sb.setSpan(
AbsoluteSizeSpan(moreTextSize, true), 3, sb.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
if (moreCanClick){
//設定點擊事件
sb.setSpan(
MyClickSpan(context, onAllSpanClickListener), 3, sb.length,
Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
}
return Triple(layout, stringBuilder, sb)
}
private fun getOffsetWidth(
indexEnd: Int,
offset: Int,
tempText: CharSequence,
countEmoji: Int,
offsetWidth: Int,
layout: Layout,
moreWidth: Int
): Pair<Int, Int> {
var offset1 = offset
var offsetWidth1 = offsetWidth
if (indexEnd > offset1) {
val text = tempText[indexEnd - offset1 - 1].toString().trim()
if (text.isNotEmpty() && countEmoji % 2 == 0) {
val charText = tempText[indexEnd - offset1]
offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
//一個表情兩個字符,避免截取一半字符出現亂碼或者顯示不全...全文
if (offsetWidth1 > moreWidth && PublicMethod.isEmojiCharacter(charText)) {
offset1++
}
}
} else {
val charText = tempText[indexEnd - offset1]
offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
}
return Pair(offset1, offsetWidth1)
}
/**
* 獲取內容高度
*/
private fun getDesiredHeight(layout: Layout?): Int {
if (layout == null) {
return 0
}
val lineTop: Int
val lineCount = layout.lineCount
val compoundPaddingTop = compoundPaddingTop + compoundPaddingBottom - lineSpacingExtra.toInt()
lineTop = when {
lineCount > maxLine -> {
//文字行數超過最大行
layout.getLineTop(maxLine)
}
else -> {
layout.getLineTop(lineCount)
}
}
return (lineTop + compoundPaddingTop).coerceAtLeast(suggestedMinimumHeight)
}
大概思路就是判斷內容行數大于我們想要的內容行數時進行內容的裁剪,內容最后顯示的文案moreText可以按照需求配置,我們測量出moreText的寬度,從最大行數的最后一個文字向前遍歷截取,直至截取文字的寬度大于等于moreText的寬度,然后我們通過使用SpannableString來拼接moreText文案和moreText的點擊事件,這里還處理了截取到表情字符的情況,我們知道一個表情兩個字符,如果正好截取到表情的一半可以放下moreText就會導致表情變成一個?的亂碼,
另外這里,我們設定了moreText的點擊事件,那如果textView本身需要設定點擊事件怎么辦?這個時候就需要處理觸摸事件了,代碼如下:
override fun onTouchEvent(event: MotionEvent): Boolean {
val text = text
val spannable = Spannable.Factory.getInstance().newSpannable(text)
if (event.action == MotionEvent.ACTION_DOWN) {
//手指按下
onDown(spannable, event)
}
if (mPressedSpan != null && mPressedSpan is MyLinkClickSpan) {
//如果有MyLinkClickSpan就走MyLinkMovementMethod的onTouchEvent
return MyLinkMovementMethod.instance
.onTouchEvent(this, text as Spannable, event)
}
if (event.action == MotionEvent.ACTION_MOVE) {
//手指移動
val mClickSpan = getPressedSpan(this, spannable, event)
if (mPressedSpan != null && mPressedSpan !== mClickSpan) {
mPressedSpan = null
Selection.removeSelection(spannable)
}
}
if (event.action == MotionEvent.ACTION_UP) {
//手指抬起
onUp(event, spannable)
}
return result
}
/**
* 手指按下邏輯
*/
private fun onDown(spannable: Spannable, event: MotionEvent) {
//按下時記下clickSpan
mPressedSpan = getPressedSpan(this, spannable, event)
if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
result = true
Selection.setSelection(
spannable, spannable.getSpanStart(mPressedSpan),
spannable.getSpanEnd(mPressedSpan)
)
} else {
result = if (moreCanClick){
super.onTouchEvent(event)
}else{
false
}
}
}
/**
* 手指抬起邏輯
*/
private fun onUp(event: MotionEvent, spannable: Spannable?) {
result = if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
(mPressedSpan as MyClickSpan).onClick(this)
true
} else {
if (moreCanClick) {
super.onTouchEvent(event)
}
false
}
mPressedSpan = null
Selection.removeSelection(spannable)
}
/**
* 設定尾部...全文點擊事件
*/
fun setOnAllSpanClickListener(
onAllSpanClickListener: MyClickSpan.OnAllSpanClickListener
) {
this.onAllSpanClickListener = onAllSpanClickListener
}
private fun getPressedSpan(
textView: TextView, spannable: Spannable,
event: MotionEvent
): ClickableSpan? {
var mTouchSpan: ClickableSpan? = 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 = layout
val line = layout.getLineForVertical(y)
val off = layout.getOffsetForHorizontal(line, x.toFloat())
val spans: Array<MyClickSpan> =
spannable.getSpans(
off, off,
MyClickSpan::class.java
)
if (spans.isNotEmpty()) {
mTouchSpan = spans[0]
} else {
val linkSpans = spannable.getSpans(off, off, MyLinkClickSpan::class.java)
if (linkSpans != null && linkSpans.isNotEmpty()) {
mTouchSpan = linkSpans[0]
}
}
return mTouchSpan
}
其中
if (mPressedSpan != null && mPressedSpan is MyLinkClickSpan) {
//如果有MyLinkClickSpan就走MyLinkMovementMethod的onTouchEvent
return MyLinkMovementMethod.instance
.onTouchEvent(this, text as Spannable, event)
}
是對鏈接的兼容處理,如果對這個有疑問請看我的上一篇關于鏈接描述的文章#Android 仿微博正文鏈接互動
三、完整代碼
class ListMoreTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.MoreTextViewStyle
) :
AppCompatTextView(context, attrs, defStyleAttr) {
/**
* 最大行數
*/
private var maxLine: Int
private val moreTextSize: Int
/**
* 尾部更多文字
*/
private val moreText: String?
/**
* 尾部更多文字顏色
*/
private val moreTextColor: Int
/**
* 是否可以點擊尾部更多文字
*/
private val moreCanClick : Boolean
private var mPaint: Paint? = null
/**
* 尾部更多文字點擊事件介面回呼
*/
private var onAllSpanClickListener: MyClickSpan.OnAllSpanClickListener? = null
/**
* 實作span的點擊
*/
private var mPressedSpan: ClickableSpan? = null
private var result = false
init {
val array = getContext().obtainStyledAttributes(
attrs,
R.styleable.ListMoreTextView, defStyleAttr, 0
)
maxLine = array.getInt(R.styleable.MoreTextView_more_action_text_maxLines, Int.MAX_VALUE)
moreText = array.getString(R.styleable.MoreTextView_more_action_text)
moreTextSize = array.getInteger(R.styleable.MoreTextView_more_action_text_size, 13)
moreTextColor = array.getColor(R.styleable.MoreTextView_more_action_text_color, Color.BLACK)
moreCanClick = array.getBoolean(R.styleable.MoreTextView_more_can_click,false)
array.recycle()
init()
}
private fun init() {
mPaint = paint
}
/**
* 設定最大行數
*/
fun setMaxLine (maxLine : Int){
this.maxLine = maxLine
}
/**
* 使用者主動呼叫
* 如果有顯示鏈接需求一定要呼叫此方法
*/
fun setMovementMethodDefault() {
movementMethod = MyLinkMovementMethod.instance
highlightColor = Color.TRANSPARENT
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (lineCount > maxLine) {
//如果大于設定的最大行數
val (layout, stringBuilder, sb) = clipContent()
stringBuilder.append(sb)
setMeasuredDimension(measuredWidth, getDesiredHeight(layout))
text = stringBuilder
}
}
/**
* 裁剪內容
*/
private fun clipContent(): Triple<Layout, SpannableStringBuilder, SpannableString> {
var offset = 1
val layout = layout
val staticLayout = StaticLayout(
text,
layout.paint,
layout.width,
Layout.Alignment.ALIGN_NORMAL,
layout.spacingMultiplier,
layout.spacingAdd,
false
)
val indexEnd = staticLayout.getLineEnd(maxLine - 1)
val tempText = text.subSequence(0, indexEnd)
var offsetWidth =
layout.paint.measureText(tempText[indexEnd - 1].toString()).toInt()
val moreWidth =
ceil(layout.paint.measureText(moreText).toDouble()).toInt()
//表情位元組個數
var countEmoji = 0
while (indexEnd > offset && offsetWidth <= moreWidth ) {
//當前位元組是否位表情
val isEmoji = PublicMethod.isEmojiCharacter(tempText[indexEnd - offset])
if (isEmoji){
countEmoji += 1
}
offset++
val pair = getOffsetWidth(
indexEnd,
offset,
tempText,
countEmoji,
offsetWidth,
layout,
moreWidth
)
offset = pair.first
offsetWidth = pair.second
}
val ssbShrink = tempText.subSequence(0, indexEnd - offset)
val stringBuilder = SpannableStringBuilder(ssbShrink)
val sb = SpannableString(moreText)
sb.setSpan(
ForegroundColorSpan(moreTextColor), 3, sb.length,
Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
//設定字體大小
sb.setSpan(
AbsoluteSizeSpan(moreTextSize, true), 3, sb.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
if (moreCanClick){
//設定點擊事件
sb.setSpan(
MyClickSpan(context, onAllSpanClickListener), 3, sb.length,
Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
}
return Triple(layout, stringBuilder, sb)
}
private fun getOffsetWidth(
indexEnd: Int,
offset: Int,
tempText: CharSequence,
countEmoji: Int,
offsetWidth: Int,
layout: Layout,
moreWidth: Int
): Pair<Int, Int> {
var offset1 = offset
var offsetWidth1 = offsetWidth
if (indexEnd > offset1) {
val text = tempText[indexEnd - offset1 - 1].toString().trim()
if (text.isNotEmpty() && countEmoji % 2 == 0) {
val charText = tempText[indexEnd - offset1]
offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
//一個表情兩個字符,避免截取一半字符出現亂碼或者顯示不全...全文
if (offsetWidth1 > moreWidth && PublicMethod.isEmojiCharacter(charText)) {
offset1++
}
}
} else {
val charText = tempText[indexEnd - offset1]
offsetWidth1 += layout.paint.measureText(charText.toString()).toInt()
}
return Pair(offset1, offsetWidth1)
}
/**
* 獲取內容高度
*/
private fun getDesiredHeight(layout: Layout?): Int {
if (layout == null) {
return 0
}
val lineTop: Int
val lineCount = layout.lineCount
val compoundPaddingTop = compoundPaddingTop + compoundPaddingBottom - lineSpacingExtra.toInt()
lineTop = when {
lineCount > maxLine -> {
//文字行數超過最大行
layout.getLineTop(maxLine)
}
else -> {
layout.getLineTop(lineCount)
}
}
return (lineTop + compoundPaddingTop).coerceAtLeast(suggestedMinimumHeight)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
val text = text
val spannable = Spannable.Factory.getInstance().newSpannable(text)
if (event.action == MotionEvent.ACTION_DOWN) {
//手指按下
onDown(spannable, event)
}
if (mPressedSpan != null && mPressedSpan is MyLinkClickSpan) {
//如果有MyLinkClickSpan就走MyLinkMovementMethod的onTouchEvent
return MyLinkMovementMethod.instance
.onTouchEvent(this, text as Spannable, event)
}
if (event.action == MotionEvent.ACTION_MOVE) {
//手指移動
val mClickSpan = getPressedSpan(this, spannable, event)
if (mPressedSpan != null && mPressedSpan !== mClickSpan) {
mPressedSpan = null
Selection.removeSelection(spannable)
}
}
if (event.action == MotionEvent.ACTION_UP) {
//手指抬起
onUp(event, spannable)
}
return result
}
/**
* 手指按下邏輯
*/
private fun onDown(spannable: Spannable, event: MotionEvent) {
//按下時記下clickSpan
mPressedSpan = getPressedSpan(this, spannable, event)
if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
result = true
Selection.setSelection(
spannable, spannable.getSpanStart(mPressedSpan),
spannable.getSpanEnd(mPressedSpan)
)
} else {
result = if (moreCanClick){
super.onTouchEvent(event)
}else{
false
}
}
}
/**
* 手指抬起邏輯
*/
private fun onUp(event: MotionEvent, spannable: Spannable?) {
result = if (mPressedSpan != null && mPressedSpan is MyClickSpan) {
(mPressedSpan as MyClickSpan).onClick(this)
true
} else {
if (moreCanClick) {
super.onTouchEvent(event)
}
false
}
mPressedSpan = null
Selection.removeSelection(spannable)
}
/**
* 設定尾部...全文點擊事件
*/
fun setOnAllSpanClickListener(
onAllSpanClickListener: MyClickSpan.OnAllSpanClickListener
) {
this.onAllSpanClickListener = onAllSpanClickListener
}
private fun getPressedSpan(
textView: TextView, spannable: Spannable,
event: MotionEvent
): ClickableSpan? {
var mTouchSpan: ClickableSpan? = 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 = layout
val line = layout.getLineForVertical(y)
val off = layout.getOffsetForHorizontal(line, x.toFloat())
val spans: Array<MyClickSpan> =
spannable.getSpans(
off, off,
MyClickSpan::class.java
)
if (spans.isNotEmpty()) {
mTouchSpan = spans[0]
} else {
val linkSpans = spannable.getSpans(off, off, MyLinkClickSpan::class.java)
if (linkSpans != null && linkSpans.isNotEmpty()) {
mTouchSpan = linkSpans[0]
}
}
return mTouchSpan
}
}
<declare-styleable name="ListMoreTextView">
<attr name="more_action_text_maxLines" format="integer"/>
<attr name="more_action_text" format="string"/>
<attr name="more_action_text_color" format="color"/>
<attr name="more_action_text_size" format="integer"/>
<attr name="more_can_click" format="boolean"/>
</declare-styleable>
注意:如果是有鏈接需求要主動呼叫該方法,否則鏈接的觸摸互動無效,
/**
* 使用者主動呼叫
* 如果有顯示鏈接需求一定要呼叫此方法
*/
fun setMovementMethodDefault() {
movementMethod = MyLinkMovementMethod.instance
highlightColor = Color.TRANSPARENT
}
另外,這里沒有對內容連續換行的處理,因為個人覺得串列資料是對主要內容的顯示,另外客戶端不要做太多的資料處理的耗時操作,應該是由后端的同學或者產品設計時避免這種情況的產生,
四、代碼地址
點擊獲取
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/374818.html
標籤:其他
