The effect is as follows:
Directly on the code:
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.Layout;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
/**
* 在超长文本后面加一个view,用来控制文本展示区域,当展开状态时超出的文本将可垂直滚动
* 非长文本内容就像TextView一样
*
* @author ly
* date 2020/3/9 10:22
*/
public class ScrollExpandTextView extends ScrollView implements ViewTreeObserver.OnGlobalLayoutListener {
private static final int MAX_LINE_COLLAPSE = 2;//收起时最大展示行数
private static final String TEXT_EXPAND = "展开";
private static final String TEXT_COLLAPSE = "收起";
private static final int MAX_H = 3 * PixelUtil.dp2px(50);
private int w;
private int minH;
private int curH;
private int arrowSize;
private TextView tv;
private TextView tvMore;
private State state = State.COLLAPSE;
private CharSequence text;
private LayoutParams paramsMore;
private int space;
public enum State {
EXPAND, COLLAPSE
}
public ScrollExpandTextView(@NonNull Context context) {
super(context);
init();
}
public ScrollExpandTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ScrollExpandTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(w, curH);//固定宽高
}
private void init() {
w = (int) (0.7 * ScreenUtil.getScreenWidth());
minH = PixelUtil.dp2px(50);
curH = minH;
arrowSize = PixelUtil.dp2px(7);
space = PixelUtil.dp2px(3);
FrameLayout flContainer = new FrameLayout(getContext());
tv = new TextView(getContext());
tv.setTextSize(14);
tv.setMaxHeight(PixelUtil.dp2px(50));
tv.setMaxLines(MAX_LINE_COLLAPSE);
tv.setIncludeFontPadding(true);
tv.setLineSpacing(1.0f, 1.2f);
tvMore = new TextView(getContext());
tvMore.setBackgroundResource(R.drawable.scroll_expand_tv_more);
tvMore.setMaxLines(1);
tvMore.setTextSize(12);
tvMore.setGravity(Gravity.CENTER);
tvMore.setVisibility(GONE);
tvMore.setOnClickListener(v -> {
if (state == State.COLLAPSE) {
state = State.EXPAND;
tv.setMaxLines(Integer.MAX_VALUE);
tv.setText(text);
} else {
state = State.COLLAPSE;
tv.setMaxLines(MAX_LINE_COLLAPSE);
}
setMoreViewPosition();
});
tv.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
paramsMore = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
tvMore.setLayoutParams(paramsMore);
flContainer.addView(tv);
flContainer.addView(tvMore);
addView(flContainer);
setTextColor(ContextCompat.getColor(getContext(), R.color.white));
setMoreTextColor(ContextCompat.getColor(getContext(), R.color.white));
setOverScrollMode(OVER_SCROLL_NEVER);
setVerticalScrollBarEnabled(false);
}
private void setMoreViewPosition() {
Layout layout = tv.getLayout();
if (layout == null)
return;
int lineCount = layout.getLineCount();
int lineH = layout.getLineBottom(0) - layout.getLineTop(0);
minH = MAX_LINE_COLLAPSE * lineH;
curH = lineCount * lineH;
if (text == null || lineCount <= MAX_LINE_COLLAPSE && tv.length() == text.length()) {
tvMore.setVisibility(GONE);
} else {
if (state == State.COLLAPSE) {
curH = minH;
float lineWidth = layout.getLineWidth(MAX_LINE_COLLAPSE - 1);
//获取第2行最后一个字符的下标
int lineEnd = layout.getLineEnd(MAX_LINE_COLLAPSE - 1);
//计算每个字符占的宽度
float widthPerChar = layout.getLineWidth(MAX_LINE_COLLAPSE - 1) / (lineEnd + 1);
float diff = lineWidth + tvMore.getMeasuredWidth() + space - (getWidth() - getPaddingLeft() - getPaddingRight());
//第二行展示不下,去掉第二行最后几个字符,用来放展开按钮
if (diff > 0) {
int removeCount = (int) (diff / widthPerChar);
if (lineEnd > removeCount) {
CharSequence t = text.subSequence(0, lineEnd - removeCount) + "...";
setTextAndRefresh(t);
return;//setText会重新触发onGlobalLayout
}
}
//获取第二行字符的坐标,设置展开按钮的margin,使展开按钮在文本后面
paramsMore.leftMargin = (int) layout.getLineRight(MAX_LINE_COLLAPSE - 1) + space;
paramsMore.topMargin = lineH + tv.getPaddingTop() - space;
tvMore.setText(TEXT_EXPAND);
drawRight4MoreView(R.drawable.ico_arrowdown);
} else {
if (curH > MAX_H)
curH = MAX_H;
float lineWidth = layout.getLineWidth(lineCount - 1);
if (lineWidth + tvMore.getMeasuredWidth() - (getWidth() - getPaddingLeft() - getPaddingRight()) > 0) {//最后一行显示不下,将最后一行换行
if (text.length() > 2) {
//分两个字符到tvMore那一行,更协调
String tmp = text.subSequence(0, text.length() - 2) + "\n" + text.subSequence(text.length() - 2, text.length());
setTextAndRefresh(tmp);
return;//setText会重新触发onGlobalLayout
}
}
tvMore.setText(TEXT_COLLAPSE);
drawRight4MoreView(R.drawable.ico_arrowup);
paramsMore.leftMargin = (int) layout.getSecondaryHorizontal(layout.getLineEnd(lineCount - 1)) + space;
paramsMore.topMargin = layout.getHeight() - tv.getPaddingBottom() - lineH + PixelUtil.dp2px(2);
}
tvMore.setVisibility(VISIBLE);
}
getLayoutParams().height = curH;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setMoreViewPosition();
}
@Override
public void onGlobalLayout() {
//为保证TextView.getLayout()!=null,在这里再执行相关逻辑
setMoreViewPosition();
//记得移除,不然会一直回调
tv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
public void setText(final CharSequence text) {
this.text = text;
setTextAndRefresh(text);
}
public void setTextAndRefresh(CharSequence text) {
tv.getViewTreeObserver().addOnGlobalLayoutListener(this);
tv.setText(text);
}
private void drawRight4MoreView(int icRes) {
Drawable drawable = getResources().getDrawable(icRes);
/// 这一步必须要做,否则不会显示.
drawable.setBounds(arrowSize / 3, 0, arrowSize, arrowSize / 3);
tvMore.setCompoundDrawables(null, null, drawable, null);
}
public void setTextColor(int color) {
tv.setTextColor(color);
}
public void setMoreTextColor(int color) {
tvMore.setTextColor(color);
}
//如果不需要处理滑动冲突,去掉下面的代码即可
private int startX, startY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) ev.getX();
startY = (int) ev.getY();
getParent().requestDisallowInterceptTouchEvent(false);
break;
case MotionEvent.ACTION_MOVE:
int endX = (int) ev.getX();
int endY = (int) ev.getY();
int disX = Math.abs(endX - startX);
int disY = Math.abs(endY - startY);
if (disX > disY) {
getParent().requestDisallowInterceptTouchEvent(canScrollHorizontally(startX - endX));
} else {
getParent().requestDisallowInterceptTouchEvent(canScrollVertically(startY - endY));
}
break;
default:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
}
Copy the code
The above