不断学习,做更好的自己!💪
视频号CSDN简书欢迎打开微信,关注我的视频号:KevinDev点我点我 前言我们经常会碰到这样一个需求:文本内容过多,可以展开和收起。
效果图
注意:
- 显示 “…展开” 时,是截取的一定行数之后,在最后一行的末尾直接显示;
- “收起” 显示在全部文本的下一行,并且是右对齐;
- 展开和收起的动画效果。
1. ExpandTextView.java
public class ExpandTextView extends AppCompatTextView {
public static final String ELLIPSIS_STRING = new String(new char[]{'\u2026'});
private static final int DEFAULT_MAX_LINE = 3;
private static final String DEFAULT_OPEN_SUFFIX = " 展开";
private static final String DEFAULT_CLOSE_SUFFIX = " 收起";
volatile boolean animating = false;
boolean isClosed = false;
private int mMaxLines = DEFAULT_MAX_LINE;
/** TextView可展示宽度,包含paddingLeft和paddingRight */
private int initWidth = 0;
/** 原始文本 */
private CharSequence originalText;
private SpannableStringBuilder mOpenSpannableStr, mCloseSpannableStr;
private boolean hasAnimation = false;
private Animation mOpenAnim, mCloseAnim;
private int mOpenHeight, mCLoseHeight;
private boolean mExpandable;
private boolean mCloseInNewLine;
@Nullable
private SpannableString mOpenSuffixSpan, mCloseSuffixSpan;
private String mOpenSuffixStr = DEFAULT_OPEN_SUFFIX;
private String mCloseSuffixStr = DEFAULT_CLOSE_SUFFIX;
private int mOpenSuffixColor, mCloseSuffixColor;
private View.OnClickListener mOnClickListener;
private CharSequenceToSpannableHandler mCharSequenceToSpannableHandler;
public ExpandTextView(Context context) {
this(context,null);
}
public ExpandTextView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public ExpandTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
/** 初始化 */
private void initialize() {
mOpenSuffixColor = mCloseSuffixColor = Color.parseColor("#F23030");
setMovementMethod(OverLinkMovementMethod.getInstance());
setIncludeFontPadding(false);
updateOpenSuffixSpan();
updateCloseSuffixSpan();
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
public void setOriginalText(CharSequence originalText) {
this.originalText = originalText;
mExpandable = false;
mCloseSpannableStr = new SpannableStringBuilder();
final int maxLines = mMaxLines;
SpannableStringBuilder tempText = charSequenceToSpannable(originalText);
mOpenSpannableStr = charSequenceToSpannable(originalText);
if (maxLines != -1) {
Layout layout = createStaticLayout(tempText);
mExpandable = layout.getLineCount() > maxLines;
if (mExpandable) {
//拼接展开内容
if (mCloseInNewLine) {
mOpenSpannableStr.append("\n");
}
if (mCloseSuffixSpan != null) {
mOpenSpannableStr.append(mCloseSuffixSpan);
}
//计算原文截取位置
int endPos = layout.getLineEnd(maxLines - 1);
if (originalText.length() maxLines) {
int lastSpace = mCloseSpannableStr.length() - 1;
if (lastSpace == -1) {
break;
}
if (originalText.length() = 0 && originalText.length() > lastSpace){
CharSequence redundantChar = originalText.subSequence(lastSpace, lastSpace + mOpenSuffixSpan.length());
int offset = hasEnCharCount(redundantChar) - hasEnCharCount(mOpenSuffixSpan) + 1;
lastSpace = offset = Build.VERSION_CODES.JELLY_BEAN) {
return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL,
getLineSpacingMultiplier(), getLineSpacingExtra(), getIncludeFontPadding());
}else{
return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL,
getFloatField("mSpacingMult",1f), getFloatField("mSpacingAdd",0f), getIncludeFontPadding());
}
}
private float getFloatField(String fieldName,float defaultValue){
float value = defaultValue;
if(TextUtils.isEmpty(fieldName)){
return value;
}
try {
// 获取该类的所有属性值域
Field[] fields = this.getClass().getDeclaredFields();
for (Field field:fields) {
if(TextUtils.equals(fieldName,field.getName())){
value = field.getFloat(this);
break;
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return value;
}
/**
* @param charSequence
*
* @return
*/
private SpannableStringBuilder charSequenceToSpannable(@NonNull CharSequence charSequence) {
SpannableStringBuilder spannableStringBuilder = null;
if (mCharSequenceToSpannableHandler != null) {
spannableStringBuilder = mCharSequenceToSpannableHandler.charSequenceToSpannable(charSequence);
}
if (spannableStringBuilder == null) {
spannableStringBuilder = new SpannableStringBuilder(charSequence);
}
return spannableStringBuilder;
}
/**
* 初始化TextView的可展示宽度
*
* @param width
*/
public void initWidth(int width) {
initWidth = width;
}
@Override
public void setMaxLines(int maxLines) {
this.mMaxLines = maxLines;
super.setMaxLines(maxLines);
}
/**
* 设置展开后缀text
*
* @param openSuffix
*/
public void setOpenSuffix(String openSuffix) {
mOpenSuffixStr = openSuffix;
updateOpenSuffixSpan();
}
/**
* 设置展开后缀文本颜色
*
* @param openSuffixColor
*/
public void setOpenSuffixColor(@ColorInt int openSuffixColor) {
mOpenSuffixColor = openSuffixColor;
updateOpenSuffixSpan();
}
/**
* 设置收起后缀text
*
* @param closeSuffix
*/
public void setCloseSuffix(String closeSuffix) {
mCloseSuffixStr = closeSuffix;
updateCloseSuffixSpan();
}
/**
* 设置收起后缀文本颜色
*
* @param closeSuffixColor
*/
public void setCloseSuffixColor(@ColorInt int closeSuffixColor) {
mCloseSuffixColor = closeSuffixColor;
updateCloseSuffixSpan();
}
/**
* 收起后缀是否另起一行
*
* @param closeInNewLine
*/
public void setCloseInNewLine(boolean closeInNewLine) {
mCloseInNewLine = closeInNewLine;
updateCloseSuffixSpan();
}
/** 更新展开后缀Spannable */
private void updateOpenSuffixSpan() {
if (TextUtils.isEmpty(mOpenSuffixStr)) {
mOpenSuffixSpan = null;
return;
}
mOpenSuffixSpan = new SpannableString(mOpenSuffixStr);
mOpenSuffixSpan.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, mOpenSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mOpenSuffixSpan.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
switchOpenClose();
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(mOpenSuffixColor);
ds.setUnderlineText(false);
}
},0, mOpenSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
}
/** 更新收起后缀Spannable */
private void updateCloseSuffixSpan() {
if (TextUtils.isEmpty(mCloseSuffixStr)) {
mCloseSuffixSpan = null;
return;
}
mCloseSuffixSpan = new SpannableString(mCloseSuffixStr);
mCloseSuffixSpan.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, mCloseSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (mCloseInNewLine) {
AlignmentSpan alignmentSpan = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE);
mCloseSuffixSpan.setSpan(alignmentSpan, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
mCloseSuffixSpan.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull View widget) {
switchOpenClose();
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(mCloseSuffixColor);
ds.setUnderlineText(false);
}
},1, mCloseSuffixStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
@Override
public void setOnClickListener(View.OnClickListener onClickListener) {
mOnClickListener = onClickListener;
}
public OpenAndCloseCallback mOpenCloseCallback;
public void setOpenAndCloseCallback(OpenAndCloseCallback callback){
this.mOpenCloseCallback = callback;
}
public interface OpenAndCloseCallback{
void onOpen();
void onClose();
}
/**
* 设置文本内容处理
*
* @param handler
*/
public void setCharSequenceToSpannableHandler(CharSequenceToSpannableHandler handler) {
mCharSequenceToSpannableHandler = handler;
}
public interface CharSequenceToSpannableHandler {
@NonNull
SpannableStringBuilder charSequenceToSpannable(CharSequence charSequence);
}
class ExpandCollapseAnimation extends Animation {
private final View mTargetView;//动画执行view
private final int mStartHeight;//动画执行的开始高度
private final int mEndHeight;//动画结束后的高度
ExpandCollapseAnimation(View target, int startHeight, int endHeight) {
mTargetView = target;
mStartHeight = startHeight;
mEndHeight = endHeight;
setDuration(400);
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
mTargetView.setScrollY(0);
//计算出每次应该显示的高度,改变执行view的高度,实现动画
mTargetView.getLayoutParams().height = (int) ((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight);
mTargetView.requestLayout();
}
}
}
2. OverLinkMovementMethod.java
public class OverLinkMovementMethod extends LinkMovementMethod {
public static boolean canScroll = false;
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_MOVE){
if(!canScroll){
return true;
}
}
return super.onTouchEvent(widget, buffer, event);
}
public static MovementMethod getInstance() {
if (sInstance == null)
sInstance = new OverLinkMovementMethod();
return sInstance;
}
private static OverLinkMovementMethod sInstance;
private static Object FROM_BELOW = new NoCopySpan.Concrete();
}
3. 使用
- 布局文件
- 代码使用
public class WidgetActivity extends AppCompatActivity {
@BindView(R.id.etv_content)
ExpandTextView mContent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_widget);
ButterKnife.bind(this);
int width = getWindowManager().getDefaultDisplay().getWidth() - dp2px(this, 20f);
mContent.initWidth(width);
mContent.setMaxLines(3);
mContent.setHasAnimation(true);
mContent.setCloseInNewLine(true);
mContent.setOpenSuffixColor(getResources().getColor(R.color.teal_200));
mContent.setCloseSuffixColor(getResources().getColor(R.color.teal_200));
mContent.setOriginalText("世界上总有一些人的出生是不被期待的。\n" +
"在那个年代如火如荼的计划生育中,总有一两条漏网之鱼。\n" +
"南方的春天阴雨绵绵,空气中弥漫着浓郁的湿气,让人不由得觉得烦躁,难受。\n" +
"一个女人挺着大肚子,东躲西藏,四处打听,吃尽了闭门羹。所幸皇天不负有心人,\n " +
"几番周折后,她终于在临盆前的一天找到了愿意接生的医生。"
);
}
public static int dp2px(Context context, float dpValue) {
int res = 0;
final float scale = context.getResources().getDisplayMetrics().density;
if (dpValue
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?