前言:当文本过长时(超过指定行数),打点省略显示并在其后添加“展开”,点击则展开显示全部,再次点击收起,效果如下
分析:(1)超过n行折叠 (2)“【展开】”内嵌至文本中且通过颜色标识(3)点击展开收缩
方案一:
计算n行的总长度,获取“【更多】”所占长度,两者差值即为能容纳的字符串长度,截取字符串之后再拼接上“【更多】”即可
object TextUtil{
/**
* textView:文本控件
* textStr:文本内容
* lineWidth:行宽度
* minLine:最小行数
* endStr:尾部追加字符
* endColor:尾部字符颜色
*/
fun ellipsize(textView: TextView, textStr: String, lineWidth: Float, minLine: Int, endStr: String, endColor: Int){
//最小行数可容文本长度
val availableTextWidth = lineWidth * minLine
val newStr = TextUtils.ellipsize(textStr, textView.paint, availableTextWidth, TextUtils.TruncateAt.END)
if (newStr.length < textStr.length){
//文本超过限制行数
val endTextWidth = textView.paint.measureText(endStr)
val resultStr = TextUtils.ellipsize(textStr, textView.paint, availableTextWidth - endTextWidth, TextUtils.TruncateAt.END).toString() + endStr
val ssb = SpannableStringBuilder(resultStr)
ssb.setSpan(ForegroundColorSpan(endColor), ssb.length - endStr.length, ssb.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
textView.text = ssb
textView.tag = ssb
}else{
textView.text = textStr
}
}
}
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
val textStr = "我志愿加入中国共产党,拥护党的纲领,遵守党的章程,履行党员义务,执行党的决定,严守党的纪律,保守党的秘密,对党忠诚,积极工作,为共产主义奋斗终身,随时准备为党和人民牺牲一切,永不叛党。"
val lineWidth = dpTopx(220f)
TextUtil.ellipsize(tv, textStr, lineWidth, 2, "【展开】", resources.getColor(R.color.colorAccent))
tv.setOnClickListener {
if (it.tag != null){//文本需要折叠展开效果
if (tv.text.toString() == textStr){
tv.text = tv.tag as SpannableStringBuilder
}else{
tv.text = textStr
}
}
}
}
fun dpTopx(dp: Float): Float{
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)
}
}
主要api:
(1)获取指定长度能容纳的字符串,超长则截取并将尾部字符替换为省略符
TextUtils.ellipsize(textStr, textView.paint, availableTextWidth, TextUtils.TruncateAt.END)
(2)测量字符串所占宽度
textView.paint.measureText(endStr)
总结:方案看似很完美,然而却翻车,首先不支持换行符,另外还有换行的一些规则(如换行后首字符是标点则会将上一行的尾部字符拉下来显示,如下图),该方案只是提供了一个思路,如果想解决该问题,可以重新TextView,再onDraw中按照我们测量的规则进行绘制即可
方案二:
布局完整字符串,总行数超过指定行则获取指定行末尾字符位置,通过该位置获取到新字符串(追加上省略符和结束语),然后再布局新字符串循环处理直到行数不超过指定行为止
object TextUtil {
/**
* textView:文本控件
* textStr:文本内容
* lineWidth:行宽度
* minLine:最小行数
* endStr:尾部追加字符
* endColor:尾部字符颜色
*/
fun ellipsize(textView: TextView, textStr: String, lineWidth: Float, minLine: Int, endStr: String, endColor: Int) {
val layout = createLayout(textView, textStr, lineWidth.toInt())
if (layout.lineCount > minLine) {
//获取指定行最后位置
var endPosition = layout.getLineEnd(minLine - 1)
var showStr = "${textStr.substring(0, endPosition)}...$endStr"
var newLayout = createLayout(textView, showStr, lineWidth.toInt())
while (newLayout.lineCount > minLine){
endPosition--
showStr = "${textStr.substring(0, endPosition)}...$endStr"
newLayout = createLayout(textView, showStr, lineWidth.toInt())
}
val ssb = SpannableStringBuilder(showStr)
ssb.setSpan(ForegroundColorSpan(endColor), ssb.length - endStr.length, ssb.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
textView.text = ssb
textView.tag = ssb
} else {
textView.text = textStr
}
}
private fun createLayout(textView: TextView, textStr: String, lineWidth: Int): StaticLayout {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
StaticLayout(textStr, textView.paint, lineWidth, Layout.Alignment.ALIGN_NORMAL,
textView.lineSpacingMultiplier, textView.lineSpacingExtra, textView.includeFontPadding)
} else {
val builder = StaticLayout.Builder.obtain(textStr, 0, textStr.length, textView.paint, lineWidth)
builder.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(textView.lineSpacingExtra, textView.lineSpacingMultiplier)
.setIncludePad(textView.includeFontPadding)
.setBreakStrategy(textView.breakStrategy)
.setHyphenationFrequency(textView.hyphenationFrequency)
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
builder.setJustificationMode(textView.justificationMode)
}
builder.build()
}
}
}
主要api:
(1)获取指定行最后位置
layout.getLineEnd(minLine - 1)
(2)获取总行数
layout.lineCount
总结:由于TextView的测量规则也是通过StaticLayout,保证了一致性,所以不会发生方案一的问题,方案一可以通过重写TextView的onDraw,依据我们的测量规则进行绘制来保持一致性则可解决。