package net.avenwu.support.widget; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.text.Html; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.widget.TextView; import net.avenwu.support.R; import java.io.UnsupportedEncodingException; import java.lang.reflect.Field; /** * Created by chaobin on 11/18/15. */ @TargetApi(11) public class ExTextView extends TextView implements View.OnClickListener, ValueAnimator.AnimatorUpdateListener { static final String TAG = ExTextView.class.getCanonicalName(); static final int END = 0; static final int RIGHT = 1; int mMaxHeight; int mCollapsedHeight; int mMaxLine; boolean isCollapsed; boolean isLayout = false; boolean isMeasured = false; ValueAnimator mExpandAnimator; CharSequence mCollapsedText; CharSequence mFullText; Drawable mIndicator; OnClickListener mOuterListener; Style mStyle; public ExTextView(Context context) { this(context, null); } public ExTextView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ExTextView); isCollapsed = a.getBoolean(R.styleable.ExTextView_expand_collapse_default, true); mIndicator = a.getDrawable(R.styleable.ExTextView_expand_indicator); if (mIndicator != null) { mIndicator.setBounds(0, 0, mIndicator.getIntrinsicWidth(), mIndicator.getIntrinsicHeight ()); } int style = a.getInt(R.styleable.ExTextView_expand_style, 0); switch (style) { case END: mStyle = new EndStyle(); break; case RIGHT: mStyle = new RightStyle(); break; } a.recycle(); reflectMaxLines(); super.setOnClickListener(this); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Log.d(TAG, "onMeasure"); if (!TextUtils.isEmpty(mFullText) && !isMeasured) { Log.d(TAG, "onMeasure isCollapsed=" + isCollapsed); if (isCollapsed) { setMaxLines(Integer.MAX_VALUE); super.onMeasure(widthMeasureSpec, heightMeasureSpec); mMaxHeight = getMeasuredHeight(); setMaxLines(mMaxLine); super.onMeasure(widthMeasureSpec, heightMeasureSpec); mCollapsedHeight = getMeasuredHeight(); } else { setMaxLines(mMaxLine); super.onMeasure(widthMeasureSpec, heightMeasureSpec); mCollapsedHeight = getMeasuredHeight(); setMaxLines(Integer.MAX_VALUE); super.onMeasure(widthMeasureSpec, heightMeasureSpec); mMaxHeight = getMeasuredHeight(); } isMeasured = true; } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (!TextUtils.isEmpty(mFullText) && (!isLayout && isExpandable())) { Log.d(TAG, "onLayout reset text"); mStyle.onLayout(isCollapsed, this, mIndicator); if (isCollapsed) { if (mCollapsedText == null) { mCollapsedText = mStyle.collapsedText(this, mIndicator, mFullText, mMaxLine); } super.setText(mCollapsedText, reflectCurrentBufferType()); } else { super.setText(mFullText, reflectCurrentBufferType()); } isLayout = true; } else { super.onLayout(changed, left, top, right, bottom); } } private void reflectMaxLines() { try { Field maximumField = TextView.class.getDeclaredField("mMaximum"); maximumField.setAccessible(true); mMaxLine = maximumField.getInt(this); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } private BufferType reflectCurrentBufferType() { try { Field field = TextView.class.getDeclaredField("mBufferType"); field.setAccessible(true); return (BufferType) field.get(this); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return BufferType.NORMAL; } @Override public void setText(CharSequence text, BufferType type) { //view 复用的时候会重复绑定数据 if (TextUtils.isEmpty(text)) { super.setText(text, type); } else if (text.equals(mFullText)) { super.setText(isCollapsed ? mCollapsedText : mFullText, reflectCurrentBufferType()); } else { mFullText = text; mCollapsedText = null; isLayout = false; isMeasured = false; super.setText(mFullText, type); } } @Override public void onClick(View v) { if (mOuterListener != null) { mOuterListener.onClick(v); } if (!isExpandable()) { return; } if (mExpandAnimator != null) { mExpandAnimator.end(); } isLayout = false; int from = getHeight(); int to; if (from < mMaxHeight) { to = mMaxHeight; isCollapsed = false; setMaxLines(Integer.MAX_VALUE); } else { to = mCollapsedHeight; isCollapsed = true; setMaxLines(mMaxLine); } if (mExpandAnimator == null) { mExpandAnimator = ValueAnimator.ofInt(from, to); mExpandAnimator.setInterpolator(new DecelerateInterpolator()); mExpandAnimator.addUpdateListener(this); } else { mExpandAnimator.setIntValues(from, to); } mExpandAnimator.start(); } @Override public void onAnimationUpdate(ValueAnimator animation) { getLayoutParams().height = (Integer) animation.getAnimatedValue(); requestLayout(); } private boolean isExpandable() { return mCollapsedHeight != mMaxHeight; } @Override public void setOnClickListener(OnClickListener listener) { mOuterListener = listener; } public interface Style { CharSequence collapsedText(TextView textView, Drawable drawable, CharSequence text, int maxLine); void onLayout(boolean collapsed, TextView textView, Drawable drawable); } private static class EndStyle implements Style { static final String HTML_IMG = "...<img src='icon'/>"; static final String HTML_NEW_LINE = "<br>"; @Override public CharSequence collapsedText(TextView view, final Drawable drawable, CharSequence text, int maxLine) { StringBuilder stringBuilder = new StringBuilder(); int start = 0; // 由于中英文字符等排版问题断行具有不确定性,此处强行对缩略文本断行 for (int i = 0; i < maxLine; i++) { int end = view.getLayout().getLineVisibleEnd(i); String append; if (i == maxLine - 1) { end -= 3; append = HTML_IMG; } else { append = HTML_NEW_LINE; } stringBuilder.append(text.subSequence(start, end)).append(append); start = end; } String subString = stringBuilder.toString(); return Html.fromHtml(subString, new Html.ImageGetter() { @Override public Drawable getDrawable(String source) { return "icon".equals(source) ? drawable : null; } }, null); } @Override public void onLayout(boolean collapsed, TextView textView, Drawable drawable) { } } private static class RightStyle implements Style { @Override public CharSequence collapsedText(TextView view, Drawable drawable, CharSequence text, int maxLine) { int end = view.getLayout().getLineVisibleEnd(maxLine - 1); return text.subSequence(0, end - 3) + "..."; } @Override public void onLayout(boolean collapsed, TextView textView, Drawable drawable) { Drawable[] d = textView.getCompoundDrawables(); if (collapsed) { d[2] = drawable; } else { d[2] = null; } textView.setCompoundDrawables(d[0], d[1], d[2], d[3]); } } }