一
widget
package widget;
import android.content.Context;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.AppCompatTextView;
import android.text.Layout;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import com.self.zsp.mutualhelp.R;
import util.LogUtils;
import value.Magic;
/**
* Created on 2018/3/31.
* 静/动设
*
* @author xxx
* @desc 隐显
*/
public class LittleMoreTextView extends AppCompatTextView {
public LittleMoreTextView(Context context) {
super(context, null);
}
public LittleMoreTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
limitTextViewString(this.getText().toString(), 140, this, new OnClickListener() {
@Override
public void onClick(View v) {
// 设监听函数
}
});
}
/**
* 动设
*
* @param content
*/
public void setContent(String content) {
limitTextViewString(content, 140, this, new OnClickListener() {
@Override
public void onClick(View v) {
// 设监听函数
}
});
}
/**
* get the last char index for max limit row,if not exceed the limit, return -1
*
* @param textView
* @param content
* @param width
* @param maxLine
* @return
*/
private int getLastCharIndexForLimitTextView(TextView textView, String content, int width, int maxLine) {
TextPaint textPaint = textView.getPaint();
StaticLayout staticLayout = new StaticLayout(content, textPaint, width, Layout.Alignment.ALIGN_NORMAL, 1, 0, false);
if (staticLayout.getLineCount() > maxLine) {
// exceed
return staticLayout.getLineStart(maxLine) - 1;
} else {
// not exceed the max line
return -1;
}
}
/**
* 限TextView显字符且添showMore和showLittle点击事件
*
* @param textString
* @param textView
* @param clickListener textView点击监听器
*/
private void limitTextViewString(String textString, int maxFirstShowCharCount, final TextView textView, final OnClickListener clickListener) {
// 算处理花费时间
final long startTime = System.currentTimeMillis();
if (textView == null) {
return;
}
// RecyclerView和ListView中,因复用原因,该TextView可能以前即画好,可获宽
int width = textView.getWidth();
if (width == 0) {
// 获TextView实际宽,可用各方式(通dp转px写死)填TextView宽
width = 1000;
}
int lastCharIndex = getLastCharIndexForLimitTextView(textView, textString, width, 4);
// 返-1表没达行限
if (lastCharIndex < 0 && textString.length() <= maxFirstShowCharCount) {
// 行数没超限
textView.setText(textString);
return;
}
// 行数超限
// this will deprive the recyclerView's focus
textView.setMovementMethod(LinkMovementMethod.getInstance());
if (lastCharIndex > maxFirstShowCharCount || lastCharIndex < 0) {
lastCharIndex = maxFirstShowCharCount;
}
// 构造spannableString
String explicitText = null;
String explicitTextAll;
if (textString.charAt(lastCharIndex) == Magic.CHAR_N) {
// manual enter
explicitText = textString.substring(0, lastCharIndex);
} else if (lastCharIndex > Magic.INT_SE) {
// 最大行数限制的那行达12后直显更多
explicitText = textString.substring(0, lastCharIndex - 12);
}
int sourceLength = explicitText.length();
String showMore = getContext().getString(R.string.textViewMore);
explicitText = explicitText + "..." + showMore;
final SpannableString mSpan = new SpannableString(explicitText);
String dismissMore = getContext().getString(R.string.packUp);
explicitTextAll = textString + " " + dismissMore;
final SpannableString mSpanALL = new SpannableString(explicitTextAll);
mSpanALL.setSpan(new ClickableSpan() {
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
// 收起色
/*ds.setColor(textView.getResources().getColor(R.color.colorPrimary));*/
ds.setColor(ContextCompat.getColor(getContext(), R.color.orange));
ds.setAntiAlias(true);
ds.setUnderlineText(false);
}
@Override
public void onClick(View widget) {
textView.setText(mSpan);
textView.setOnClickListener(null);
new Handler().postDelayed(() -> {
if (clickListener != null) {
// prevent the double click
textView.setOnClickListener(clickListener);
}
}, 20);
}
}, textString.length(), explicitTextAll.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mSpan.setSpan(new ClickableSpan() {
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
// 更多色
/*ds.setColor(textView.getResources().getColor(R.color.colorPrimary));*/
ds.setColor(ContextCompat.getColor(getContext(), R.color.orange));
ds.setAntiAlias(true);
ds.setUnderlineText(false);
}
@Override
public void onClick(View widget) {
//"...show more" click event
textView.setText(mSpanALL);
textView.setOnClickListener(null);
new Handler().postDelayed(() -> {
if (clickListener != null) {
// prevent the double click
textView.setOnClickListener(clickListener);
}
}, 20);
}
}, sourceLength, explicitText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 显更多状TextView
textView.setText(mSpan);
LogUtils.e("info", "字符串处理耗时" + (System.currentTimeMillis() - startTime) + " ms");
}
}
布局
<widget.LittleMoreTextView
android:id="@+id/tvActiveDetailTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/fontHint"
android:textSize="@dimen/s13" />
主代码
tvActiveDetailTitle.setContent(dataBean.getAcTitle());
二
widget
package widget;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.v4.content.ContextCompat;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ImageSpan;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.TextView;
import com.self.zsp.mutualhelp.R;
import util.DensityUtils;
import value.Magic;
/**
* @decs: 两模式、三对齐、一padding
* @date: 2018/5/27 0027 上午 9:50
* @version: v 1.0
*/
public class ExpandTextView extends android.support.v7.widget.AppCompatTextView implements View.OnClickListener {
/**
* 默显8行
*/
private static final int MAX_COLLAPSED_LINES = 8;
/**
* 默动时长300ms
*/
private static final int DEFAULT_ANIM_DURATION = 300;
/**
* the default alpha value when the animation starts
*/
private static final float DEFAULT_ANIM_ALPHA_START = 0.7f;
/**
* 最大显行数
*/
private int mMaxCollapsedLines = 8;
private int mAnimationDuration;
private float mAnimAlphaStart;
/**
* 展前图
*/
private Drawable mExpandDrawable;
/**
* 展后图
*/
private Drawable mCollapseDrawable;
private int mCollapsedHeight;
private int mTextHeightWithMaxLines;
/**
* 此刻折叠状
*/
private boolean mCollapsed = true;
private boolean mAnimating = false;
/**
* 需折叠已显尾图否
*/
private boolean needCollapse = true;
private int mDrawableSize = 0;
/**
* 箭头对齐方式(左/上、居中、右/下)
*/
private static final int ALIGN_RIGHT_BOTTOM = 0;
private static final int ALIGN_LEFT_TOP = 1;
private static final int ALIGN_CENTER = 2;
private int arrowAlign = ALIGN_RIGHT_BOTTOM;
/**
* 箭头显位(文右、文下)
*/
private static final int POSITION_RIGHT = 0;
private static final int POSITION_BELOW = 1;
private int arrowPosition = POSITION_RIGHT;
/**
* 图文间距
*/
private int arrowDrawablePadding = 0;
/**
* listener for callback
*/
private OnExpandStateChangeListener mListener;
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);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ExpandTextView, defStyleAttr, 0);
mMaxCollapsedLines = typedArray.getInt(R.styleable.ExpandTextView_maxCollapsedLines, MAX_COLLAPSED_LINES);
mAnimationDuration = typedArray.getInt(R.styleable.ExpandTextView_animDuration, DEFAULT_ANIM_DURATION);
mAnimAlphaStart = typedArray.getFloat(R.styleable.ExpandTextView_animAlphaStart, DEFAULT_ANIM_ALPHA_START);
mExpandDrawable = typedArray.getDrawable(R.styleable.ExpandTextView_expandDrawable);
mCollapseDrawable = typedArray.getDrawable(R.styleable.ExpandTextView_collapseDrawable);
arrowAlign = typedArray.getInteger(R.styleable.ExpandTextView_arrowAlign, ALIGN_RIGHT_BOTTOM);
arrowPosition = typedArray.getInteger(R.styleable.ExpandTextView_arrowPosition, POSITION_RIGHT);
arrowDrawablePadding = (int) typedArray.getDimension(R.styleable.ExpandTextView_arrowPadding, DensityUtils.dip2px(context, 2f));
typedArray.recycle();
if (mExpandDrawable == null) {
mExpandDrawable = getDrawable(getContext(), R.drawable.right);
}
if (mCollapseDrawable == null) {
mCollapseDrawable = getDrawable(getContext(), R.drawable.right);
}
setClickable(true);
setOnClickListener(this);
}
private boolean isDrawablePaddingResolved = false;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (getVisibility() == GONE || mAnimating) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
// 重置高重测
// 设wrap_content(重新measure)
getLayoutParams().height = -2;
setMaxLines(Integer.MAX_VALUE);
// 测TextView总高
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getLineCount() <= mMaxCollapsedLines) {
needCollapse = false;
return;
}
needCollapse = true;
mTextHeightWithMaxLines = getRealTextViewHeight(this);
if (mCollapsed) {
setMaxLines(mMaxCollapsedLines);
}
mDrawableSize = mExpandDrawable.getIntrinsicWidth();
if (!isDrawablePaddingResolved) {
if (arrowPosition == POSITION_RIGHT) {
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight() + mDrawableSize + arrowDrawablePadding, getPaddingBottom());
} else {
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom() + mExpandDrawable.getIntrinsicHeight() + arrowDrawablePadding);
}
isDrawablePaddingResolved = true;
}
// 设后重测
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mCollapsed) {
mCollapsedHeight = getMeasuredHeight();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (needCollapse) {
int left, top;
if (arrowPosition == POSITION_RIGHT) {
left = getWidth() - getTotalPaddingRight() + arrowDrawablePadding;
switch (arrowAlign) {
case ALIGN_LEFT_TOP:
top = getTotalPaddingTop();
break;
case ALIGN_CENTER:
top = (getHeight() - mExpandDrawable.getIntrinsicHeight()) / 2;
break;
case ALIGN_RIGHT_BOTTOM:
default:
top = getHeight() - getTotalPaddingBottom() - mExpandDrawable.getIntrinsicHeight();
break;
}
} else {
top = getHeight() - getTotalPaddingBottom() + arrowDrawablePadding;
switch (arrowAlign) {
case ALIGN_LEFT_TOP:
left = getTotalPaddingLeft();
break;
case ALIGN_CENTER:
left = (getWidth() - mExpandDrawable.getIntrinsicWidth()) / 2;
break;
case ALIGN_RIGHT_BOTTOM:
default:
left = getWidth() - getTotalPaddingRight() - mExpandDrawable.getIntrinsicWidth();
break;
}
}
canvas.translate(left, top);
if (mCollapsed) {
mExpandDrawable.setBounds(0, 0, mExpandDrawable.getIntrinsicWidth(), mExpandDrawable.getIntrinsicHeight());
mExpandDrawable.draw(canvas);
} else {
mCollapseDrawable.setBounds(0, 0, mCollapseDrawable.getIntrinsicWidth(), mCollapseDrawable.getIntrinsicHeight());
mCollapseDrawable.draw(canvas);
}
}
}
@Override
public void setText(CharSequence text, BufferType type) {
setCollapsed(true);
super.setText(text, type);
}
@Override
public void onClick(View v) {
if (!needCollapse) {
// 行数不足(不响应点事件)
return;
}
mCollapsed = !mCollapsed;
Bitmap collapseBM = Bitmap.createBitmap(mCollapseDrawable.getIntrinsicWidth(), mCollapseDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas cv2 = new Canvas(collapseBM);
mCollapseDrawable.setBounds(0, 0, mCollapseDrawable.getIntrinsicWidth(), mCollapseDrawable.getIntrinsicHeight());
mCollapseDrawable.draw(cv2);
ImageSpan isExpand = new ImageSpan(mExpandDrawable);
ImageSpan isCollapse = new ImageSpan(getContext(), collapseBM);
SpannableString spannableString = new SpannableString("icon");
spannableString.setSpan(mCollapsed ? isExpand : isCollapse, 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// mark that the animation is in progress
mAnimating = true;
Animation animation;
if (mCollapsed) {
animation = new ExpandCollapseAnimation(this, getHeight(), mCollapsedHeight);
} else {
animation = new ExpandCollapseAnimation(this, getHeight(), mTextHeightWithMaxLines);
}
animation.setFillAfter(true);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
if (mListener != null) {
mListener.onChangeStateStart(!mCollapsed);
}
applyAlphaAnimation(ExpandTextView.this, mAnimAlphaStart);
}
@Override
public void onAnimationEnd(Animation animation) {
// clear animation here to avoid repeated applyTransformation() calls
clearAnimation();
// clear the animation flag
mAnimating = false;
// notify the listener
if (mListener != null) {
mListener.onExpandStateChanged(ExpandTextView.this, !mCollapsed);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
clearAnimation();
startAnimation(animation);
}
private class ExpandCollapseAnimation extends Animation {
private final View mTargetView;
private final int mStartHeight;
private final int mEndHeight;
ExpandCollapseAnimation(View view, int startHeight, int endHeight) {
mTargetView = view;
mStartHeight = startHeight;
mEndHeight = endHeight;
setDuration(mAnimationDuration);
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
final int newHeight = (int) ((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight);
mTargetView.getLayoutParams().height = newHeight;
setMaxHeight(newHeight);
if (Float.compare(mAnimAlphaStart, Magic.FLOAT_YDL) != 0) {
applyAlphaAnimation(ExpandTextView.this, mAnimAlphaStart + interpolatedTime * (1.0f - mAnimAlphaStart));
}
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
}
@Override
public boolean willChangeBounds() {
return true;
}
}
private Drawable getDrawable(Context context, int drawableResId) {
Resources resources = context.getResources();
if (isPostLollipop()) {
return resources.getDrawable(drawableResId, context.getTheme());
} else {
return ContextCompat.getDrawable(context, drawableResId);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void applyAlphaAnimation(View view, float alpha) {
if (isPostHoneycomb()) {
view.setAlpha(alpha);
} else {
AlphaAnimation alphaAnimation = new AlphaAnimation(alpha, alpha);
// make it instant
alphaAnimation.setDuration(0);
alphaAnimation.setFillAfter(true);
view.startAnimation(alphaAnimation);
}
}
private boolean isPostHoneycomb() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}
private boolean isPostLollipop() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
private int getRealTextViewHeight(TextView textView) {
int textHeight = textView.getLayout().getLineTop(textView.getLineCount());
int padding = textView.getCompoundPaddingTop() + textView.getCompoundPaddingBottom();
return textHeight + padding;
}
public void setCollapsed(boolean isCollapsed) {
mCollapsed = isCollapsed;
}
public interface OnExpandStateChangeListener {
/**
* xxx
*
* @param willExpand 将展
*/
void onChangeStateStart(boolean willExpand);
/**
* called when the expand/collapse animation has been finished
*
* @param textView textView being expanded/collapsed
* @param isExpanded true if the TextView has been expanded
*/
void onExpandStateChanged(TextView textView, boolean isExpanded);
}
public void setOnExpandStateChangeListener(OnExpandStateChangeListener listener) {
mListener = listener;
}
}
attrs
<!--ExpandTextView-->
<declare-styleable name="ExpandTextView">
<attr name="maxCollapsedLines" format="integer" />
<attr name="animDuration" format="integer" />
<attr name="animAlphaStart" format="float" />
<attr name="expandDrawable" format="reference" />
<attr name="collapseDrawable" format="reference" />
<!--三对齐 左上/居中/右下-->
<attr name="arrowAlign" format="enum">
<enum name="bottom" value="0" />
<enum name="top" value="1" />
<enum name="center" value="2" />
<enum name="right" value="0" />
<enum name="left" value="1" />
</attr>
<!--两模式 右/下-->
<attr name="arrowPosition" format="enum">
<enum name="right" value="0" />
<enum name="below" value="1" />
</attr>
<attr name="arrowPadding" format="dimension" />
</declare-styleable>
布局
<widget.ExpandTextView
android:id="@+id/tvBillDetailContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:textColor="@color/fontHint"
android:textSize="@dimen/s13"
app:arrowAlign="left"
app:arrowPosition="below"
app:collapseDrawable="@drawable/ic_arrow_up"
app:expandDrawable="@drawable/ic_arrow_down"
app:maxCollapsedLines="3" />
属性
maxCollapsedLines 开始折叠行数
animDuration 展开/折叠动画时长
animAlphaStart 透明度渐变起始值
expandDrawable 展开图
collapseDrawable 折叠图
arrowAlign 对齐方式(左/上、居中、右/下可选)
arrowPosition 位置(text右、text下)
arrowPadding 图文间距
注意
布局不设
app:collapseDrawable="@drawable/ic_arrow_up"
app:expandDrawable="@drawable/ic_arrow_down"
则用widget设图
if (mCollapsed) {
mExpandDrawable.setBounds(0, 0, mExpandDrawable.getIntrinsicWidth(), mExpandDrawable.getIntrinsicHeight());
mExpandDrawable.draw(canvas);
} else {
mCollapseDrawable.setBounds(0, 0, mCollapseDrawable.getIntrinsicWidth(), mCollapseDrawable.getIntrinsicHeight());
mCollapseDrawable.draw(canvas);
}