注:该文章参考于https://github.com/RayeWang/MyView
开启我的造轮子之路了,在我看来想要掌握一个新的知识都要经历这么几个阶段:了解-->抄袭-->模仿(改造)-->创造,而且自定义View对于码农来说是一个必须得掌握的一项技能get√,这个技能的难度还是挺大的(myStatus = 抄袭中)。这个将是我的一个开始,相信量变一定会引起质变。
话不多说,先上源码,里面注释我都加的挺详细的。、
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
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 com.hypertian.firstlinecode.R;
/**
* Created by hypertian on 2016/7/27.
* Desc 可收缩TextView
*/
public class CollapsedTextView extends TextView {
private static final String TAG = CollapsedTextView.class.getSimpleName();
/**
* 文本出现与消失的速度 行/ms
*/
private static final int SPEED = 10;
/**
* 截取后,文本末尾的字符串
*/
private static final String ELLIPSE = "...";
/**
* 默认全文的Text
*/
private static final String EXPANDED_TEXT = "全文";
/**
* 默认收起的text
*/
private static final String COLLAPSED_TEXT = "收起";
/**
* 真实的text
*/
private String text;
/**
* 全文的Text
*/
private String expandedText = EXPANDED_TEXT;
/**
* 收起的text
*/
private String collapsedText = COLLAPSED_TEXT;
/**
* 收起时实际显示的text
*/
private CharSequence collapsedCs;
/**
* 收起时所显示做大行数
*/
private int maxLine = 2;
/**
* 所有行数
*/
private int allLines = 0;
/**
* 收起和全文的监听类
*/
private ReadMoreClickableSpan viewMoreSpan = new ReadMoreClickableSpan();
/**
* 是否是收起状态,默认收起
*/
private boolean isCollapsed = true;
public CollapsedTextView(Context context) {
this(context, null);
}
public CollapsedTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CollapsedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
/**
* 获取自定义属性
*/
private void init(Context context, AttributeSet attrs) {
if (null != attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CollapsedTextView);
try {
allLines = ta.getInt(R.styleable.CollapsedTextView_trimLines, 0);
maxLine = ta.getInt(R.styleable.CollapsedTextView_maxLines, 2);
expandedText = ta.getString(R.styleable.CollapsedTextView_expandedText);
if (TextUtils.isEmpty(expandedText)) {
expandedText = EXPANDED_TEXT;
}
collapsedText = ta.getString(R.styleable.CollapsedTextView_collapsedText);
if (TextUtils.isEmpty(collapsedText)) {
collapsedText = COLLAPSED_TEXT;
}
} finally {
ta.recycle();
}
}
}
/**
* 设置显示的文字
*/
public void setShowText(final String text) {
this.text = text;
if (allLines > 0) {
/**
* 注册一个回调函数,当在一个视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变时调用这个回调函数。
* 参数 listener 将要被添加的回调函数
* 异常 IllegalStateException
*
* 利用OnGlobalLayoutListener来获得一个TextView的真实高度
*/
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
/**
* ViewTreeObserver不能直接实例化,而是通过getViewTreeObserver()获得
*/
ViewTreeObserver obs = getViewTreeObserver();
/**
* Build.VERSION.SDK_INT 版本号
* Build.VERSION_CODES.JELLY_BEAN 4.1版
* 监听到一次之后就取消该监听,否则将会已经进行回调
*/
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() - getPaddingLeft() - getPaddingRight();
/**
* 计算行数 = 要显示文字的总长度 / 实际每行显示的长度
*/
int lines = (int) (width / showWidth);
if (width % showWidth != 0) {
lines++;
}
if (lines > maxLine) {
//一行显示的字数
int oneLineNumbers = text.length() / lines;
int end = 0;
int lastLineEnd = 0;
//计算 "...全文" 宽度
int expandedTextWidth = (int) tp.measureText(ELLIPSE + expandedText);
//计算每行显示文本数
for (int i = 0; i < maxLine; i++) {
int tempWidth = 0;
if (i == maxLine) {
tempWidth = expandedTextWidth;
}
end += oneLineNumbers;
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 ssb = new SpannableStringBuilder(text, 0, end)
.append(ELLIPSE)
.append(expandedText);
collapsedCs = addClickableSpan(ssb, expandedText);
setText(collapsedCs);
/**
* TextView 滑动
* TextView 布局里:android:scrollbars="vertical"
* 代码里:TextView.setMovementMethod(ScrollingMovementMethod.getInstance());
*/
setMovementMethod(LinkMovementMethod.getInstance());
} else {
setText(text);
}
allLines = getLineCount();
}
});
} else {
setText(text);
}
}
@Override
public TextPaint getPaint() {
return super.getPaint();
}
private CharSequence addClickableSpan(SpannableStringBuilder s, CharSequence trimText) {
/**
* setSpan()参数
* object what :对应的各种Span
* int start:开始应用指定Span的位置,索引从0开始
* int end:结束应用指定Span的位置,特效并不包括这个位置。比如如果这里数为3(即第4个字符),第4个字符不会有任何特效
* int flags:取值有如下四个
* Spannable.SPAN_EXCLUSIVE_EXCLUSIVE :前后都不包括,即在指定范围的前面和后面插入新字符都不会应用新样式
* Spannable.SPAN_EXCLUSIVE_INCLUSIVE :前面不包括,后面包括。即仅在范围字符的后面插入新字符时会应用新样式
* Spannable.SPAN_INCLUSIVE_EXCLUSIVE :前面包括,后面不包括。
* Spannable.SPAN_INCLUSIVE_INCLUSIVE :前后都包括。
*/
s.setSpan(viewMoreSpan, s.length() - trimText.length(), s.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return s;
}
private class ReadMoreClickableSpan extends ClickableSpan {
@Override
public void onClick(View view) {
if (isCollapsed) {
/**
* SpannableString、SpannableStringBuilder基本上与String差不多,
* 也是用来存储字符串,但它们俩的特殊就在于有一个SetSpan()函数,
* 能给这些存储的String添加各种格式或者称样式(Span),将原来的String以不同的样式显示出来,
* 比如在原来String上加下划线、加背景色、改变字体颜色、用图片把指定的文字给替换掉,等等
*/
SpannableStringBuilder ssb = new SpannableStringBuilder(text).append(collapsedText);
setText(addClickableSpan(ssb, collapsedText));
setMaxLines(maxLine);
expandedHandler.sendEmptyMessage(2);
} else {
setText(text);
collapsedHandler.sendEmptyMessage(getLineCount());
}
isCollapsed = !isCollapsed;
}
}
/**
* 展开全文
*/
Handler expandedHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
int currLines = msg.what;
if (currLines <= getLineCount()) {
/**
* 通过设置显示的做大行数去达到动画的效果,收起全文同样
*/
setMaxLines(currLines);
expandedHandler.sendEmptyMessageDelayed(currLines + 1, SPEED);
}
}
};
/**
* 收起全文
*/
Handler collapsedHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
int currLines = msg.what;
if (currLines > maxLine) {
setMaxLines(currLines);
collapsedHandler.sendEmptyMessageDelayed(currLines - 1, SPEED);
} else {
setText(collapsedCs);
setMovementMethod(LinkMovementMethod.getInstance());
}
}
};
}
什么?你说开工的时候不知道都要定义那些变量,创建那些方法?其实这个问题涉及的知识面比较广,我也不知道。你要是知道了那你就有架构师的思想了,恭喜你。你难道没听过万事开头难,中间难,最后也难吗?可以先不去管那些变量,方法什么的,先做一个简易版的效果出来,然后在慢慢改进,优化等,到最后你就什么都知道了。
再来附上values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="textSize" format="dimension"/>
<attr name="textColor" format="color"/>
<attr name="expandedText" format="string"/>
<attr name="collapsedText" format="string"/>
<attr name="trimLines" format="integer"/>
<attr name="maxLines" format="integer"/>
<declare-styleable name="CollapsedTextView">
<attr name="expandedText"/>
<attr name="collapsedText"/>
<attr name="trimLines"/>
<attr name="maxLines"/>
</declare-styleable>
</resources>
最后就是使用了,使用自定义属性,一定要加上命名空间
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<span style="color:#ff0000;">xmlns:app="http://schemas.android.com/apk/res-auto"</span>
android:id="@+id/three_main_activity"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
<com.hypertian.firstlinecode.threechapter.CollapsedTextView
android:id="@+id/ctv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:trimLines="3"
app:maxLines="2"/>
</LinearLayout>
最后附上效果图