Files
assistant-android/app/src/main/java/com/gh/common/view/ExpandTextView.java

280 lines
11 KiB
Java

package com.gh.common.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.os.Build;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.util.AttributeSet;
import android.view.View;
import com.gh.gamecenter.R;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.ContextCompat;
public class ExpandTextView extends AppCompatTextView {
private CharSequence mSnapshotText;
private String mEndText = "...";
private String mExpandText = mEndText + "全文";
private boolean mUseGradientAlphaEndText = false;
private boolean mShrinkOnAnchor = false; // 以锚点所在的位置进行收起 (即 maxLines = 锚点所在的行)
private int mMaxLines = 3; // 由于sdk版本限制(getMaxLines) 这里设置默认值
private boolean mInitLayout = false;
private boolean mIsExpanded = false; // 位于 recyclerView 时需要自行在外层管理是否已展开
private ExpandCallback mExpandCallback;
private SelfCalculateMaxLinesCallback mMaxLinesCalculatedCallback;
private Rect mLastVisibleLineRect;
private Rect mLastActualLineRect;
private static int DEFAULT_ADDITIONAL_END_TEXT_COUNT = 2;
public static final String SHRINK_ANCHOR = "";
public ExpandTextView(Context context) {
super(context);
}
public ExpandTextView(Context context, AttributeSet attrs) {
super(context, attrs);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mMaxLines = getMaxLines();
}
mLastVisibleLineRect = new Rect();
mLastActualLineRect = new Rect();
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ExpandTextView);
mShrinkOnAnchor = ta.getBoolean(R.styleable.ExpandTextView_shrinkOnAnchor, false);
mUseGradientAlphaEndText = ta.getBoolean(R.styleable.ExpandTextView_useGradientAlphaEndText, false);
mEndText = ta.getString(R.styleable.ExpandTextView_endText) == null ? mEndText : ta.getString(R.styleable.ExpandTextView_endText);
mExpandText = ta.getString(R.styleable.ExpandTextView_expandText) == null ? mExpandText : ta
.getString(R.styleable.ExpandTextView_expandText);
ta.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mShrinkOnAnchor && getText().toString().contains(SHRINK_ANCHOR)) {
updateMaxLinesByShrinkAnchor();
}
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight() - getExtraBottomPadding());
}
private void updateMaxLinesByShrinkAnchor() {
int lineCount = getLineCount();
for (int line = 0; line < lineCount; line++) {
CharSequence substring = getText().subSequence(getLayout().getLineStart(line), getLayout().getLineEnd(line));
if (substring.toString().contains(SHRINK_ANCHOR)) {
if (mMaxLines != line + 1) {
if (!mIsExpanded) {
setExpandMaxLines(line + 1);
mMaxLinesCalculatedCallback.onMaxLinesCalculated(line + 1);
}
setText(findAndDeleteAnchor(getText()));
break;
}
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mInitLayout && !mIsExpanded && getLineCount() > mMaxLines) {
mSnapshotText = findAndDeleteAnchor(getText());
mInitLayout = false;
showExpandButton();
}
}
public void setExpandText(String text) {
this.mExpandText = text;
}
public void setExpandCallback(ExpandCallback callback) {
this.mExpandCallback = callback;
}
@Override
public void setText(CharSequence text, BufferType type) {
mInitLayout = true;
super.setText(text, type);
}
private void showExpandButton() {
String finalEndText = "";
TextPaint paint = getPaint();
Layout layout = getLayout();
int start = layout.getLineStart(0);
int lastLineEnd = layout.getLineEnd(mMaxLines - 1);
int lastLineStart = layout.getLineStart(mMaxLines - 1);
float lastLineRight = layout.getLineRight(mMaxLines - 1);
int viewWidth = getWidth() - getPaddingRight() - getPaddingLeft();
int additionalEndTextCount = 0;
float expandTextWidth;
if (mUseGradientAlphaEndText) {
additionalEndTextCount = DEFAULT_ADDITIONAL_END_TEXT_COUNT;
expandTextWidth = paint.measureText(mEndText + mExpandText);
} else {
expandTextWidth = paint.measureText(mExpandText);
}
CharSequence content = mSnapshotText.subSequence(start, lastLineEnd);
if (viewWidth - lastLineRight > expandTextWidth) {
if (mUseGradientAlphaEndText) {
finalEndText = content.toString()
.substring(content.length() - additionalEndTextCount, content.length()) + mEndText;
finalEndText = finalEndText.replace("\n", "");
content = content.subSequence(0, content.length() - additionalEndTextCount) + finalEndText + mExpandText;
} else {
content = content.toString().trim() + mExpandText;
}
} else {
CharSequence lastLineText = mSnapshotText.subSequence(lastLineStart, lastLineEnd);
CharSequence subSequence;
float subSequenceWidth;
for (int i = lastLineText.length() - 1; i > 0; i--) {
if (mUseGradientAlphaEndText) {
subSequence = lastLineText.subSequence(0, i - additionalEndTextCount);
subSequenceWidth = paint.measureText(subSequence.toString());
finalEndText = lastLineText.subSequence(i - additionalEndTextCount, i) + mEndText;
expandTextWidth = paint.measureText(finalEndText + mExpandText);
if (viewWidth - subSequenceWidth > expandTextWidth) {
finalEndText = finalEndText.replace("\n", "");
content = mSnapshotText.subSequence(start, lastLineStart + i - additionalEndTextCount) + finalEndText + mExpandText;
break;
}
} else {
subSequence = lastLineText.subSequence(0, i);
subSequenceWidth = paint.measureText(subSequence.toString());
if (viewWidth - subSequenceWidth > expandTextWidth) {
content = mSnapshotText.subSequence(start, lastLineStart + i) + mExpandText;
break;
}
}
}
}
SpannableStringBuilder msp = new SpannableStringBuilder(mSnapshotText);
int length = msp.length();
int startPosition;
startPosition = content.length() - finalEndText.length() - mExpandText.length();
startPosition = Math.max(startPosition, 0);
// 避免越界
if (startPosition >= length) return;
msp.replace(startPosition, length, finalEndText + mExpandText);
msp.setSpan(new ClickableSpan() {
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(ContextCompat.getColor(getContext(), R.color.theme_font));
ds.setUnderlineText(false);
}
@Override
public void onClick(@NonNull View widget) {
mIsExpanded = true;
setMaxLines(Integer.MAX_VALUE);
setText(mSnapshotText);
if (mExpandCallback != null) {
mExpandCallback.onExpand();
}
}
}, startPosition + mEndText.length(), msp.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
msp.setSpan(new GradientAlphaTextSpan(), startPosition, startPosition + finalEndText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
setText(msp);
setMovementMethod(CustomLinkMovementMethod.getInstance());
}
/**
* 获取 maxLines + lineSpacingExtra + movementMethod 一起使用时产生的大小与 lineSpacingExtra 一样的底部空间
*/
private int getExtraBottomPadding() {
int result = 0;
// 界面上显示的最后一行的 index
int lastVisibleLineIndex = Math.min(getMaxLines(), getLineCount()) - 1;
// 获取实际文字的最后一行的 index
int lastActualLineIndex = getLineCount() - 1;
if (lastVisibleLineIndex >= 0) {
Layout layout = getLayout();
int lastVisibleLineBaseline = getLineBounds(lastVisibleLineIndex, mLastVisibleLineRect);
getLineBounds(lastActualLineIndex, mLastActualLineRect);
int heightBetweenLastVisibleLineRectAndLastActualLineRect = (mLastActualLineRect.bottom - mLastVisibleLineRect.bottom);
if (getMeasuredHeight() == getLayout().getHeight() - heightBetweenLastVisibleLineRectAndLastActualLineRect) {
result = mLastVisibleLineRect.bottom - (lastVisibleLineBaseline + layout.getPaint()
.getFontMetricsInt().descent + getPaddingBottom());
if (getLineSpacingExtra() > result) {
result = 0;
} else {
result = (int) getLineSpacingExtra();
}
}
}
return result;
}
// 去掉锚点
private CharSequence findAndDeleteAnchor(CharSequence charSequence) {
int anchorIndex = charSequence.toString().indexOf(SHRINK_ANCHOR);
if (anchorIndex >= 0) {
SpannableStringBuilder sb = new SpannableStringBuilder(getText());
sb.delete(anchorIndex, anchorIndex + 1);
return sb;
} else {
return charSequence;
}
}
/**
* 此方法仅更改标记,不做实际展开的操作
*/
public void setIsExpanded(boolean isExpanded) {
mIsExpanded = isExpanded;
}
public void setExpandMaxLines(int maxLines) {
mMaxLines = maxLines;
setMaxLines(maxLines);
}
public void setSelfCalculateMaxLinesCallback(SelfCalculateMaxLinesCallback callback) {
mMaxLinesCalculatedCallback = callback;
}
public interface ExpandCallback {
void onExpand();
}
public interface SelfCalculateMaxLinesCallback {
void onMaxLinesCalculated(int maxLines);
}
}