平时关于展开和收起的使用效果是不少的。也有一些各路子的实现方式。关于这个实现,这里我用字符串裁剪+富文本的方式去实现。
效果图:
首先。我们要算出手机屏幕的宽度,如果在设计上两边有边距的话记得减去,如此,得到的便是TextView的宽度:
val dm = host.resources.displayMetrics
val width = (dm.widthPixels) - 两侧边距
下一步,拿到textView单个文本的大小,再用上面算出的TextView的宽度除上单个字体的大小:
val textSize = tvBrief.textSize
val linesNum = (width / textSize) * 保留显示的行数//得到两行总字数
如上,得到的就是要保留的行数的字体总和。后面可根据业务来做具体实现。如:
//如果后台返给的字数大于要保留的行数字数的总和就追加“展开”,否则全部展示
if (talker.summary.length > linesNum) {
expandText(tvBrief, talker, linesNum, "展开")
} else {
tvBrief.text = talker.summary
}
这里是expandText 和 collapseText 方法的具体实现,为裁剪追加字符串和展开收起的方法:
//收起状态,点击展开
fun expandText(tvBrief: ExpandTextView, talker: Talker, linesNum: Float, expand: String) {
val strSummary = "${talker.summary.substring(0, (linesNum - 9).toInt())}... $expand"
onChange(tvBrief, talker, linesNum, expand, strSummary)
}
///
//展开状态,点击收起
fun collapseText(tvBrief: ExpandTextView, talker: Talker, linesNum: Float, expand: String) {
val strSummary = "${talker.summary} $expand"
onChange(tvBrief, talker, linesNum, expand, strSummary)
}
下面为onChange方法,具体的样式和效果逻辑(注意:这里用自定义的TextView,会在下面把源码贴出):
/**
tvBrief:TextView控件
talker:后台返给的bean类,可以去到要截取的数据
linesNum:为保留行数的字数总和
expand:要追加的文字,如(展开,收起)
strSummary:截取并且拼接好的字符串
**/
fun onChange(tvBrief: ExpandTextView, talker: Talker, linesNum: Float, expand: String, strSummary: String) {
val spannableString = SpannableString(strSummary)
val span = RoundBackgroundColorSpan(Color.parseColor("#faa623"), Color.parseColor("#ffffff"), 12)
spannableString.setSpan(span, strSummary.length - 3, strSummary.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
spannableString.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
if (expand == "展开") {
collapseText(tvBrief, talker, linesNum, "收起")
} else {
expandText(tvBrief, talker, linesNum, "展开")
}
}
}, strSummary.length - 3, strSummary.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
tvBrief.text = spannableString
tvBrief.movementMethod = LinkMovementMethod.getInstance()
}
自定义view :ExpandTextView.java
/**
* 自定义控件,长文本展开收起TextView
*/
public class ExpandTextView extends AppCompatTextView {
/**
* 原始内容文本
*/
private String originText;
/**
* TextView最大行数
*/
private int mMaxLines = 4;
/**
* 收起的文案(颜色处理)
*/
private SpannableString SPAN_CLOSE = null;
/**
* 展开的文案(颜色处理)
*/
private SpannableString SPAN_EXPAND = null;
private final String TEXT_EXPAND = " 查看更多 v";
private final String TEXT_CLOSE = " <收起";
public ExpandTextView(Context context) {
super(context);
initCloseEnd();
}
public ExpandTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initCloseEnd();
}
public ExpandTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initCloseEnd();
}
/**
* 设置TextView可显示的最大行数
*
* @param maxLines 最大行数
*/
@Override
public void setMaxLines(int maxLines) {
this.mMaxLines = maxLines;
super.setMaxLines(maxLines);
}
/**
* 收起的文案(颜色处理)初始化
*/
private void initCloseEnd() {
String content = TEXT_EXPAND;
SPAN_CLOSE = new SpannableString(content);
ButtonSpan span = new ButtonSpan(getContext(), v -> {
ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
setExpandText(originText);
}, R.color.themeAccent);
SPAN_CLOSE.setSpan(span, 0, content.length() - 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
Drawable drawable = getResources().getDrawable(R.mipmap.ic_live_speech_summary_more);
Rect rect = new Rect(0, -8, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
drawable.setBounds(rect);
// drawable.setBounds(0, -8, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
SPAN_CLOSE.setSpan(new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE), content.length() - 1, content.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
/**
* 展开的文案(颜色处理)初始化
*/
private void initExpandEnd() {
String content = TEXT_CLOSE;
SPAN_EXPAND = new SpannableString(content);
ButtonSpan span = new ButtonSpan(getContext(), v -> {
ExpandTextView.super.setMaxLines(mMaxLines);
setCloseText(originText);
}, R.color.themeAccent);
SPAN_EXPAND.setSpan(span, 0, content.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
public void setCloseText(CharSequence text) {
if (SPAN_CLOSE == null) {
initCloseEnd();
}
// true 不需要展开收起功能, false 需要展开收起功能
boolean appendShowAll = false;
originText = text.toString();
// SDK >= 16 可以直接从xml属性获取最大行数
int maxLines;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
maxLines = getMaxLines();
} else {
maxLines = mMaxLines;
}
String workingText = originText;
if (maxLines != -1) {
Layout layout = createWorkingLayout(workingText);
if (layout.getLineCount() > maxLines) {
//获取一行显示字符个数,然后截取字符串数
// 收起状态原始文本截取展示的部分
workingText = originText.substring(0, layout.getLineEnd(maxLines - 1)).trim();
String showText = originText.substring(0, layout.getLineEnd(maxLines - 1)).trim() + "..." + SPAN_CLOSE;
Layout layout2 = createWorkingLayout(showText);
// 对workingText进行-1截取,直到展示行数==最大行数,并且添加 SPAN_CLOSE 后刚好占满最后一行
while (layout2.getLineCount() > maxLines) {
int lastSpace = workingText.length() - 1;
if (lastSpace == -1) {
break;
}
workingText = workingText.substring(0, lastSpace);
layout2 = createWorkingLayout(workingText + "..." + SPAN_CLOSE);
}
appendShowAll = true;
workingText = workingText + "...";
}
}
setText(workingText);
if (appendShowAll) {
// 必须使用append,不能在上面使用+连接,否则spannable会无效
append(SPAN_CLOSE);
setMovementMethod(LinkMovementMethod.getInstance());
}
}
public void setExpandText(String text) {
if (SPAN_EXPAND == null) {
initExpandEnd();
}
Layout layout1 = createWorkingLayout(text);
Layout layout2 = createWorkingLayout(text + TEXT_CLOSE);
// 展示全部原始内容时 如果 TEXT_CLOSE 需要换行才能显示完整,则直接将TEXT_CLOSE展示在下一行
if (layout2.getLineCount() > layout1.getLineCount()) {
setText(originText + "\n");
} else {
setText(originText);
}
append(SPAN_EXPAND);
setMovementMethod(LinkMovementMethod.getInstance());
}
/**
* 返回textview的显示区域的layout,该textview的layout并不会显示出来,只是用其宽度来比较要显示的文字是否过长
*/
private Layout createWorkingLayout(String workingText) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return new StaticLayout(workingText, getPaint(), getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
Layout.Alignment.ALIGN_NORMAL, getLineSpacingMultiplier(), getLineSpacingExtra(), false);
} else {
return new StaticLayout(workingText, getPaint(), getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
}
}
}
下面,为带圆角背景的富文本工具类实现(RoundBackgroundColorSpan.java),具体的样式可自定义富文本去实现:
public class RoundBackgroundColorSpan extends ReplacementSpan {
private int mRadius;
private int bgColor;
private int textColor;
private int mSize;
public RoundBackgroundColorSpan(int bgColor,
int textColor,
int radius) {
super();
this.bgColor = bgColor;
this.textColor = textColor;
this.mRadius = radius;
}
/**
* @param start 第一个字符的下标
* @param end 最后一个字符的下标
* @return span的宽度
*/
@Override
public int getSize(@NonNull Paint paint,
CharSequence text,
int start,
int end,
Paint.FontMetricsInt fm) {
mSize = (int) (paint.measureText(text, start, end) + 2 * mRadius);
return mSize + 5;//5:距离其他文字的空白
}
/**
* @param y baseline
*/
@Override
public void draw(@NonNull Canvas canvas,
CharSequence text,
int start,
int end,
float x,
int top,
int y, int bottom,
@NonNull Paint paint) {
int defaultColor = paint.getColor();//保存文字颜色
float defaultStrokeWidth = paint.getStrokeWidth();
//绘制圆角矩形
paint.setColor(bgColor);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(5);
paint.setAntiAlias(true);
RectF rectF = new RectF(x + 2.5f, y + 2.5f + paint.ascent(), x + mSize, y + paint.descent());
//设置文字背景矩形,x为span其实左上角相对整个TextView的x值,y为span左上角相对整个View的y值。
// paint.ascent()获得文字上边缘,paint.descent()获得文字下边缘
//x+2.5f解决线条粗细不一致问题
canvas.drawRoundRect(rectF, mRadius, mRadius, paint);
//绘制文字
paint.setColor(textColor);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(defaultStrokeWidth);
canvas.drawText(text, start, end, x + mRadius, y, paint);//此处mRadius为文字右移距离
paint.setColor(defaultColor);//恢复画笔的文字颜色
}
}