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); } }