因为公司项目需要全文收起的功能,一共有2种UI,所以需要写2个全文收起的控件,关于第一个控件已经在第一篇文章讲述 嵌套法实现全文收起TextView ,本篇文章主要讲述直接继承至TextView的实现方法
通过另外一个方法设置文本,同时在GlobalLayoutListener中计算每行出需要显示的总行数,判断是否需要全文收起功能,如果需要,则计算出每行需要显示多少文本,在设定的最大行计算时,把...+全文加进去计算,得到实际上应该显示的文本,同时把全文设置为可点击的文本,在点击事件中根据状态设置当前TextView显示的文本,如果当前状态是收起状态,点击后就设置显示所有文字+收起,全文状态则设置显示文本为最开始计算出来的文本
package wang.raye.library.widge; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.os.Build; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextPaint; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.util.AttributeSet; import android.view.View; import android.view.ViewTreeObserver; import android.widget.TextView; import wang.raye.library.R; /** * 有全文和收起的TextView ExpandableTextView * Created by Raye on 2016/6/24. */ public class CollapsedTextView extends TextView { private static final String TAG = CollapsedTextView.class.getName(); /** 收起状态下的最大行数*/ private int maxLine = 2; /** 截取后,文本末尾的字符串*/ private static final String ELLIPSE = "..."; /** 默认全文的Text*/ private static final String EXPANDEDTEXT = "全文"; /** 默认收起的text*/ private static final String COLLAPSEDTEXT = "收起"; /** 全文的text*/ private String expandedText = EXPANDEDTEXT; /** 收起的text*/ private String collapsedText = COLLAPSEDTEXT; /** 所有行数*/ private int allLines; /** 是否是收起状态,默认收起*/ private boolean collapsed = true; /** 真实的text*/ private String text; /** 收起时实际显示的text*/ private CharSequence collapsedCs; /** 全文和收起的点击事件处理*/ private ReadMoreClickableSpan viewMoreSpan = new ReadMoreClickableSpan(); public CollapsedTextView(Context context) { super(context); init(context,null); } public CollapsedTextView(Context context, AttributeSet attrs) { super(context, attrs); init(context,attrs); } public CollapsedTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context,attrs); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public CollapsedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context,attrs); } @Override public TextPaint getPaint() { return super.getPaint(); } private void init(Context context,AttributeSet attrs){ if(attrs != null){ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CollapsedTextView); allLines = ta.getInt(R.styleable.CollapsedTextView_trimLines,0); expandedText = ta.getString(R.styleable.CollapsedTextView_expandedText); if(TextUtils.isEmpty(expandedText)){ expandedText = EXPANDEDTEXT; } collapsedText = ta.getString(R.styleable.CollapsedTextView_collapsedText); if(TextUtils.isEmpty(collapsedText)){ collapsedText = COLLAPSEDTEXT; } } } public void setShowText(final String text){ this.text = text; if(allLines > 0) { getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { ViewTreeObserver obs = getViewTreeObserver(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { obs.removeOnGlobalLayoutListener(this); } else { obs.removeGlobalOnLayoutListener(this); } TextPaint tp = getPaint(); float width = tp.measureText(text); /* 计算行数 */ //获取显示宽度 int showWidth = getWidth() - getPaddingRight() - getPaddingLeft(); int lines = (int) (width / showWidth); if (width % showWidth != 0) { lines++; } allLines = (int) (tp.measureText(text + collapsedText) / showWidth); if (lines > maxLine) { int expect = text.length() / lines; int end = 0; int lastLineEnd = 0; //...+expandedText的宽度,需要在最后一行加入计算 int expandedTextWidth = (int) tp.measureText(ELLIPSE + expandedText); //计算每行显示文本数 for (int i = 1; i <= maxLine; i++) { int tempWidth = 0; if (i == maxLine) { tempWidth = expandedTextWidth; } end += expect; if (end > text.length()) { end = text.length(); } if (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) { //预期的第一行超过了实际显示的宽度 end--; while (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) { end--; } } else { end++; while (tp.measureText(text, lastLineEnd, end) < showWidth - tempWidth) { end++; } end--; } lastLineEnd = end; } SpannableStringBuilder s = new SpannableStringBuilder(text, 0, end) .append(ELLIPSE) .append(expandedText); collapsedCs = addClickableSpan(s, expandedText); setText(collapsedCs); setMovementMethod(LinkMovementMethod.getInstance()); } else { setText(text); } } }); setText(""); }else{ setText(text); } } private CharSequence addClickableSpan(SpannableStringBuilder s, CharSequence trimText) { s.setSpan(viewMoreSpan, s.length() - trimText.length(), s.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return s; } private class ReadMoreClickableSpan extends ClickableSpan { @Override public void onClick(final View widget) { if(collapsed){ SpannableStringBuilder s = new SpannableStringBuilder(text) .append(collapsedText); setText(addClickableSpan(s,collapsedText)); }else{ setText(collapsedCs); } collapsed = !collapsed; } } }
TextPaint tp = getPaint(); float width = tp.measureText(text); /* 计算行数 */ //获取显示宽度 int showWidth = getWidth() - getPaddingRight() - getPaddingLeft(); int lines = (int) (width / showWidth); if (width % showWidth != 0) { lines++; } allLines = (int) (tp.measureText(text + collapsedText) / showWidth); if (lines > maxLine) { int expect = text.length() / lines; int end = 0; int lastLineEnd = 0; //...+expandedText的宽度,需要在最后一行加入计算 int expandedTextWidth = (int) tp.measureText(ELLIPSE + expandedText); //计算每行显示文本数 for (int i = 1; i <= maxLine; i++) { int tempWidth = 0; if (i == maxLine) { tempWidth = expandedTextWidth; } end += expect; if (end > text.length()) { end = text.length(); } if (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) { //预期的第一行超过了实际显示的宽度 end--; while (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) { end--; } } else { end++; while (tp.measureText(text, lastLineEnd, end) < showWidth - tempWidth) { end++; } end--; } lastLineEnd = end; } SpannableStringBuilder s = new SpannableStringBuilder(text, 0, end) .append(ELLIPSE) .append(expandedText); collapsedCs = addClickableSpan(s, expandedText); setText(collapsedCs); setMovementMethod(LinkMovementMethod.getInstance()); } else { setText(text); }
通过TextPaint计算出文本的总宽度,粗略计算出一共需要多少行来显示,判断是否需要收起和全文功能,如果需要,则计算出每行实际展示的文本的宽度(因为通过Layout获取到的只有完全绘制成功后,才能正确获取到),同时在计算的最后一行(也就是超过多少行需要收起的最后一行),需要把"...全文"的宽度加入计算,这样才能计算出正确值,把计算出来的字符数截取出来,加入"...全文",同时针对"全文"本身,添加点击的ClickableSpan,使"全文"具有点击事件,最后设置控件展示的文本为截取的文本+"...全文",如果行数没有超过最大行数,则设置正常显示就ok了,同时保存计算出来的文本,避免再次计算。
private CharSequence addClickableSpan(SpannableStringBuilder s, CharSequence trimText) { s.setSpan(viewMoreSpan, s.length() - trimText.length(), s.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return s; } private class ReadMoreClickableSpan extends ClickableSpan { @Override public void onClick(final View widget) { if(collapsed){ SpannableStringBuilder s = new SpannableStringBuilder(text) .append(collapsedText); setText(addClickableSpan(s,collapsedText)); }else{ setText(collapsedCs); } collapsed = !collapsed; } }
通过setSpan设置"全文"的点击事件,同时通过继承ClickableSpan 来实现点击事件,事件中根据当前的状态,判断需要设置什么文本,如果是收起状态,则设置文本显示内容为实际内容+"收起",同时给收起添加点击事件,如果是全文状态,则设置显示的文本为之前计算出来的文本。
private void init(Context context,AttributeSet attrs){ if(attrs != null){ TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CollapsedTextView); allLines = ta.getInt(R.styleable.CollapsedTextView_trimLines,0); expandedText = ta.getString(R.styleable.CollapsedTextView_expandedText); if(TextUtils.isEmpty(expandedText)){ expandedText = EXPANDEDTEXT; } collapsedText = ta.getString(R.styleable.CollapsedTextView_collapsedText); if(TextUtils.isEmpty(collapsedText)){ collapsedText = COLLAPSEDTEXT; } } }
这里就很简单了,就是自义定最大多少行,"全文"的文本和"收起"的文本,相信不用多少
最近因为太忙,所以文章也写的有点水,而且总是感觉累,是身体加心累,每天躺床上就不想起床,也不喜欢敲代码,效率自热底下。同时也建议各位同行注意身体,身体才是革命的本钱,同时也要注意放松,不然心一旦累了,就很难调整过来了(对于我来说是这样),敲会代码就起身走动走动,前几天因为一直坐着敲代码,脖子痛的要命,所以适当的休息是必要的,好了,就说这么多吧,你们懂的。同时附上本控件 源码和demo github链接 ,另外同时也欢迎大家吐槽交流(QQ群:123965382)