效果
代码
ExpandTextView:
public class ExpandTextView extends LinearLayout {
private static final int COLLAPSED_LINES = 2;
private static final int DEFAULT_ANIM_DURATION = 100;
private static final float DEFAULT_ANIM_ALPHA_START = 0.7f;
private int maxCollapsedLines;
private final int animationDuration;
private final float animAlphaStart;
private int textHeightWithMaxLines;
private int marginBetweenTxtAndBottom;
private int collapsedHeight;
private boolean mRelayout = true;
private boolean mCollapsed = true;
private boolean animating;
private boolean expandable;
private TextView expandContent;
public ExpandTextView(Context context) {
this(context, null);
}
public ExpandTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public ExpandTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater.from(context).inflate(R.layout.expand_text_view, this);
expandContent = (TextView) findViewById(R.id.expand_content);
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ExpandTextView);
maxCollapsedLines = typedArray.getInt(R.styleable.ExpandTextView_collapseLine, COLLAPSED_LINES);
animationDuration = typedArray.getInt(R.styleable.ExpandTextView_animationDuration, DEFAULT_ANIM_DURATION);
animAlphaStart = typedArray.getFloat(R.styleable.ExpandTextView_animationAlphaStart, DEFAULT_ANIM_ALPHA_START);
typedArray.recycle();
setOrientation(LinearLayout.VERTICAL);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!mRelayout || getVisibility() == View.GONE) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
mRelayout = false;
// Setup with optimistic case, Everything fits. No button needed
expandContent.setMaxLines(Integer.MAX_VALUE);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// If the text fits in collapsed mode, we are done.
if (expandContent.getLineCount() <= maxCollapsedLines) {
expandable = false;
collapsedHeight = getMeasuredHeight();
} else {
expandable = true;
}
// Saves the text height with max lines
textHeightWithMaxLines = getRealTextViewHeight(expandContent);
// Doesn't fit in collapsed mode. Collapse text view as needed. Show button.
if (mCollapsed) {
expandContent.setMaxLines(maxCollapsedLines);
}
// Re-measure with new setup
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mCollapsed) {
// Gets the margin between the TextView's bottom and the ViewGroup's bottom
expandContent.post(new Runnable() {
@Override
public void run() {
marginBetweenTxtAndBottom = getHeight() - expandContent.getHeight();
}
});
// Saves the collapsed height of this ViewGroup
collapsedHeight = getMeasuredHeight();
}
}
public boolean Expandable() {
return expandable;
}
public void setMaxLines(int maxLines) {
maxCollapsedLines = maxLines;
}
private static int getRealTextViewHeight(@NonNull TextView textView) {
Layout layout = textView.getLayout();
int textHeight = 0;
int padding = 0;
if(layout != null) {
int lineCount = textView.getLineCount();
textHeight = textView.getLayout().getLineTop(lineCount);
padding = textView.getCompoundPaddingTop() + textView.getCompoundPaddingBottom();
}
return textHeight + padding;
}
public void expand() {
mCollapsed = !mCollapsed;
int startHeight = getHeight();
int endHeight = getHeight() + textHeightWithMaxLines - expandContent.getHeight();
applyAnimation(this, startHeight, endHeight, animationDuration);
}
public void collapse() {
mCollapsed = !mCollapsed;
int startHeight = getHeight();
int endHeight = collapsedHeight;
applyAnimation(this, startHeight, endHeight, animationDuration);
}
public void setText(@Nullable CharSequence text) {
expandContent.setText(text);
}
//setText and then update mRelayout value.
public void updatetText() {
mRelayout = true;
requestLayout();
invalidate();
}
/*public void setEllipsize(TextUtils.TruncateAt ellipse) {
expandContent.setEllipsize(ellipse);
}*/
public CharSequence getText() {
if (expandContent == null) {
return "";
}
return expandContent.getText();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// while an animation is in progress, intercept all the touch events to children to
// prevent extra clicks during the animation
return animating;
}
@Override
public void setOrientation(int orientation) {
if (LinearLayout.HORIZONTAL == orientation) {
throw new IllegalArgumentException("ExpandableTextView only supports Vertical Orientation.");
}
super.setOrientation(orientation);
}
private void applyAnimation(View view, int startHeight, int endHeight, int duration) {
ValueAnimator scaleAnimator = ValueAnimator.ofInt(startHeight, endHeight);
scaleAnimator.setInterpolator(new AccelerateInterpolator());
scaleAnimator.setDuration(duration);
scaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int XPoint = (int) animation.getAnimatedValue();
expandContent.setMaxHeight(XPoint - marginBetweenTxtAndBottom);
view.getLayoutParams().height = (int) XPoint;
view.requestLayout();
}
});
scaleAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
animating = true;
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
// clear the animation flag
animating = false;
// clear animation here to avoid repeated applyTransformation() calls
// clearAnimation();
scaleAnimator.cancel();
// setEllipsize(TextUtils.TruncateAt.MARQUEE);
}
});
ValueAnimator alphaAnimation = ValueAnimator.ofFloat(1f, animAlphaStart, 1f);
alphaAnimation.setDuration(duration);
AnimatorSet set = new AnimatorSet();
set.play(scaleAnimator).with(alphaAnimation);
set.start();
}
}
ExpandArrowView:
public class ExpandArrowView extends FrameLayout {
private TextView expandArrowTextView;
private String expandStr;
private String shrinkStr;
private Context context;
public ExpandArrowView(Context context) {
this(context, null);
}
public ExpandArrowView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ExpandArrowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
LayoutInflater.from(context).inflate(R.layout.expand_arrow_view, this);
expandArrowTextView = (TextView) findViewById(R.id.expand_arrow_text);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ExpandArrowView);
expandStr = ta.getString(R.styleable.ExpandArrowView_expand_arrow_text);
shrinkStr = ta.getString(R.styleable.ExpandArrowView_shrink_arrow_text);
ta.recycle();
}
public void collapse() {
if (expandStr != null) {
expandArrowTextView.setText(expandStr);
expandArrowTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_downarrow, 0);
expandArrowTextView.setTextColor(context.getResources().getColor(R.color.online_detail_header_expand_arrow_text_color));
expandArrowTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimensionPixelOffset(R.dimen.online_detail_header_expand_arrow_text_size));
}
}
public void expand() {
if (shrinkStr != null) {
expandArrowTextView.setText(shrinkStr);
expandArrowTextView.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.ic_uparrow, 0);
expandArrowTextView.setTextColor(context.getResources().getColor(R.color.online_detail_header_expand_arrow_text_color));
expandArrowTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimensionPixelOffset(R.dimen.online_detail_header_expand_arrow_text_size));
}
}
}
expand_arrow_view:
ExpandView:
public class ExpandView extends FrameLayout {
private Context context;
private ExpandTextView expandTextView;
private FrameLayout expandContentBlow;
private ExpandArrowView expandArrow;
private View spaceView;
private View rootView;
private OnlineResource trackResource;
private static int DEFAULT_LINES = 2;
private boolean expanded;
private boolean noViewShowSpace;
public ExpandView(Context context) {
this(context, null);
}
public ExpandView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ExpandView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
rootView = LayoutInflater.from(context).inflate(R.layout.expand_view, this);
expandTextView = (ExpandTextView) findViewById(R.id.expand_text_view);
expandContentBlow = (FrameLayout) findViewById(R.id.expand_content_blow);
expandArrow = (ExpandArrowView) findViewById(R.id.expand_arrow);
spaceView = findViewById(R.id.space);
rootView.setOnClickListener(v -> swapState());
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ExpandView);
DEFAULT_LINES = ta.getInteger(R.styleable.ExpandView_collapse_text_line, DEFAULT_LINES);
expanded = ta.getBoolean(R.styleable.ExpandView_default_expand, false);
noViewShowSpace = ta.getBoolean(R.styleable.ExpandView_no_view_show_space, true);
ta.recycle();
init();
}
public void setTrackResource(OnlineResource onlineResource) {
this.trackResource = onlineResource;
}
public void init() {
expandTextView.setMaxLines(DEFAULT_LINES);
this.post(() -> {
if (expanded) {
expand();
} else {
collapse();
}
});
}
public void addView(View child) {
if (child != null) {
expandContentBlow.addView(child);
}
}
public void setText(CharSequence text) {
expandTextView.setText(text);
}
public void setText(CharSequence text, View child) {
setText(text);
addView(child);
}
public static void showTextView(ExpandView textView, String str) {
if (Assertions.checkNotNull(textView)) {
if (!TextUtils.isEmpty(str)) {
textView.setText(str);
} else {
textView.setVisibility(View.GONE);
}
}
}
public static void showTextView(TextView textView, String str) {
if (Assertions.checkNotNull(textView)) {
if (!TextUtils.isEmpty(str)) {
textView.setText(str);
} else {
textView.setVisibility(View.GONE);
}
}
}
public static void showTextView(TextView textView, String foreStr, String str) {
if (Assertions.checkNotNull(textView)) {
if (!TextUtils.isEmpty(str)) {
String text = foreStr + " " + str;
textView.setText(text);
} else {
textView.setVisibility(View.GONE);
}
}
}
/*public static void showTextView(TextView textView, List resources) {
if (!ListUtils.isEmpty(resources)) {
StringBuffer sb = new StringBuffer();
for (T resource : resources) {
if (resource instanceof MusicArtist){
sb.append(((MusicArtist)resource).getName()).append(",").append(" ");
} else if (resource instanceof Album) {
sb.append(((Album)resource).getName()).append(",").append(" ");
}
}
int index = sb.toString().length() - 2;
if (index >= 0) {
sb.deleteCharAt(index);
}
textView.setText(sb);
} else {
textView.setVisibility(View.GONE);
}
}*/
private void swapState() {
if (expanded) {
OnlineTrackingUtil.trackDetailSeeMoreClicked(trackResource);
expand();
} else {
collapse();
}
}
public void collapse() {
expanded = !expanded;
String text = expandTextView.getText() != null ? expandTextView.getText().toString() : null;
boolean expandContentBlowVisible = childViewVisible(expandContentBlow);
if ("".equals(text) && expandContentBlowVisible) {
viewGone(expandTextView);
viewVisible(expandContentBlow);
expandArrow.collapse();
viewGone(expandArrow);
} else if ("".equals(text) && !expandContentBlowVisible) {
viewGone(expandTextView);
viewGone(expandContentBlow);
expandArrow.collapse();
viewGone(expandArrow);
noViewShowSpace();
} else if (!"".equals(text) && !expandContentBlowVisible) {
expandTextView.collapse();
viewVisible(expandTextView);
viewGone(expandContentBlow);
expandArrow.collapse();
if (expandTextView.Expandable()) {
viewVisible(expandArrow);
} else {
viewGone(expandArrow);
noViewShowSpace();
}
} else {
expandTextView.collapse();
viewVisible(expandTextView);
viewGone(expandContentBlow);
expandArrow.collapse();
viewVisible(expandArrow);
}
}
public void expand() {
expanded = !expanded;
String text = expandTextView.getText() != null ? expandTextView.getText().toString() : null;
boolean expandContentBlowVisible = childViewVisible(expandContentBlow);
if ("".equals(text) && expandContentBlowVisible) {
viewGone(expandTextView);
viewVisible(expandContentBlow);
expandArrow.expand();
viewGone(expandArrow);
} else if ("".equals(text) && !expandContentBlowVisible) {
viewGone(expandTextView);
viewGone(expandContentBlow);
expandArrow.expand();
viewGone(expandArrow);
noViewShowSpace();
} else if (!"".equals(text) && !expandContentBlowVisible) {
expandTextView.expand();
viewVisible(expandTextView);
viewGone(expandContentBlow);
expandArrow.expand();
if (expandTextView.Expandable()) {
viewVisible(expandArrow);
} else {
viewGone(expandArrow);
noViewShowSpace();
}
} else {
expandTextView.expand();
viewVisible(expandTextView);
viewVisible(expandContentBlow);
expandArrow.expand();
viewVisible(expandArrow);
}
}
private void noViewShowSpace() {
if (noViewShowSpace) {
spaceView.setVisibility(VISIBLE);
} else {
spaceView.setVisibility(GONE);
}
}
private boolean childViewVisible(ViewGroup parent) {
int childCount = parent.getChildCount();
if (childCount == 0) {
return false;
}
for (int i = 0; i < childCount; i++) {
View child = parent.getChildAt(i);
if (child instanceof ViewGroup) {
if (child.getVisibility() != GONE) {
return childViewVisible((ViewGroup) child);
}
} else {
if (child.getVisibility() != GONE) {
return true;
}
}
}
return false;
}
private boolean ellipsis(TextView textView) {
Layout layout = textView.getLayout();
if (layout != null) {
int lines = layout.getLineCount();
if (lines > 0) {
int ellipsisCount = layout.getEllipsisCount(lines - 1);
if (ellipsisCount > 0) {
return true;
}
}
}
return false;
}
private void viewGone(View view) {
if (view.getVisibility() != GONE) {
view.setVisibility(GONE);
}
}
private void viewVisible(View view) {
if (view.getVisibility() != VISIBLE) {
view.setVisibility(VISIBLE);
}
}
}
expand_view.xml: