最近有个需求,用户简介下面的文字可以伸缩扩展,默认显示3行文字,点击图片展开剩下的内容,ui效果如下:
于是自定义了一个可伸缩扩展的TextView ,实现效果截图如下:
实现代码如下:代码注释很详细
1.Mainivity中使用方式:
/**
* 作者: njb
* 时间: 2018/8/20 0020-下午 1:04
* 描述: 自定义可伸缩扩展的TextView
* 来源:
*/
public class MainActivity extends AppCompatActivity {
private ExpandableTextView tvLeft;//图片在左边
private ExpandableTextView tvCenter;//图片在中间
private ExpandableTextView tvRight;//图片在右边
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化控件
initView();
}
/**
* 初始化控件
*/
private void initView() {
tvLeft = findViewById(R.id.tv_left);
tvCenter = findViewById(R.id.tv_center);
tvRight = findViewById(R.id.tv_right);
//给控件赋值
tvLeft.setText(getString(R.string.left_string));
tvCenter.setText(getString(R.string.center_string));
tvRight.setText(getString(R.string.right_string));
}
}
2.自定义的ExpandableTextView
**
* 作者: njb
* 时间: 2018/8/20 0020-下午 1:04
* 描述: 自定义可伸缩扩展的TextView
* 来源:
*/
public class ExpandableTextView extends LinearLayout implements View.OnClickListener{
private static final String TAG = ExpandableTextView.class.getSimpleName();
/* The default number of lines */
private static final int MAX_COLLAPSED_LINES = 8;//默认为8,显示未被折叠的文本行数
/* The default animation duration */
private static final int DEFAULT_ANIM_DURATION = 300;//(默认为300ms)为展开/折叠的动画时间
/* The default alpha value when the animation starts */
private static final float DEFAULT_ANIM_ALPHA_START = 0.7f;//(默认为0.7f)
当动画开始时文字的透明的度。如果您想禁用透明度,设置这个值为1
protected TextView mTv;
protected ImageView mButton; //按钮展开/折叠时的图片
private boolean mRelayout;
private boolean mCollapsed = true; // 默认显示简短版本.
private int mCollapsedHeight;
private int mTextHeightWithMaxLines;
private int mMaxCollapsedLines;
private int mMarginBetweenTxtAndBottom;
private Drawable mExpandDrawable;//展开前图片
private Drawable mCollapseDrawable;//展开后图片
private int mAnimationDuration;
private float mAnimAlphaStart;
private boolean mAnimating;
/* 监听回调 */
private OnExpandStateChangeListener mListener;
/* 在列表中保存折叠状态 */
private SparseBooleanArray mCollapsedStatus;
private int mPosition;
public ExpandableTextView(Context context) {
this(context, null);
}
public ExpandableTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public ExpandableTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs);
}
@Override
public void setOrientation(int orientation){
if(LinearLayout.HORIZONTAL == orientation){
throw new IllegalArgumentException("ExpandableTextView only supports Vertical Orientation.");
}
super.setOrientation(orientation);
}
@Override
public void onClick(View view) {
if (mButton.getVisibility() != View.VISIBLE) {
return;
}
mCollapsed = !mCollapsed;
mButton.setImageDrawable(mCollapsed ? mExpandDrawable : mCollapseDrawable);
if (mCollapsedStatus != null) {
mCollapsedStatus.put(mPosition, mCollapsed);
}
// 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(), getHeight() +
mTextHeightWithMaxLines - mTv.getHeight());
}
animation.setFillAfter(true);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
applyAlphaAnimation(mTv, 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(mTv, !mCollapsed);
}
}
@Override
public void onAnimationRepeat(Animation animation) { }
});
clearAnimation();
startAnimation(animation);
}
@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 mAnimating;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
findViews();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// If no change, measure and return
if (!mRelayout || getVisibility() == View.GONE) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
mRelayout = false;
// Setup with optimistic case
// i.e. Everything fits. No button needed
mButton.setVisibility(View.GONE);
mTv.setMaxLines(Integer.MAX_VALUE);
// Measure
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// If the text fits in collapsed mode, we are done.
if (mTv.getLineCount() <= mMaxCollapsedLines) {
return;
}
// Saves the text height w/ max lines
mTextHeightWithMaxLines = getRealTextViewHeight(mTv);
// Doesn't fit in collapsed mode. Collapse text view as needed. Show
// button.
if (mCollapsed) {
mTv.setMaxLines(mMaxCollapsedLines);
}
mButton.setVisibility(View.VISIBLE);
// Re-measure with new setup
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mCollapsed) {
// Gets the margin between the TextView's bottom and the ViewGroup's bottom
mTv.post(new Runnable() {
@Override
public void run() {
mMarginBetweenTxtAndBottom = getHeight() - mTv.getHeight();
}
});
// Saves the collapsed height of this ViewGroup
mCollapsedHeight = getMeasuredHeight();
}
}
public void setOnExpandStateChangeListener(@Nullable OnExpandStateChangeListener listener) {
mListener = listener;
}
public void setText(@Nullable CharSequence text) {
mRelayout = true;
mTv.setText(text);
setVisibility(TextUtils.isEmpty(text) ? View.GONE : View.VISIBLE);
}
public void setText(@Nullable CharSequence text, @NonNull SparseBooleanArray collapsedStatus, int position) {
mCollapsedStatus = collapsedStatus;
mPosition = position;
boolean isCollapsed = collapsedStatus.get(position, true);
clearAnimation();
mCollapsed = isCollapsed;
mButton.setImageDrawable(mCollapsed ? mExpandDrawable : mCollapseDrawable);
setText(text);
getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
requestLayout();
}
@Nullable
public CharSequence getText() {
if (mTv == null) {
return "";
}
return mTv.getText();
}
private void init(AttributeSet attrs) {
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.ExpandableTextView);
mMaxCollapsedLines = typedArray.getInt(R.styleable.ExpandableTextView_maxCollapsedLines, MAX_COLLAPSED_LINES);
mAnimationDuration = typedArray.getInt(R.styleable.ExpandableTextView_animDuration, DEFAULT_ANIM_DURATION);
mAnimAlphaStart = typedArray.getFloat(R.styleable.ExpandableTextView_animAlphaStart, DEFAULT_ANIM_ALPHA_START);
mExpandDrawable = typedArray.getDrawable(R.styleable.ExpandableTextView_expandDrawable);
mCollapseDrawable = typedArray.getDrawable(R.styleable.ExpandableTextView_collapseDrawable);
if (mExpandDrawable == null) {
mExpandDrawable = getDrawable(getContext(), R.drawable.ic_keyboard_arrow_down_black_24dp);
}
if (mCollapseDrawable == null) {
mCollapseDrawable = getDrawable(getContext(), R.drawable.ic_keyboard_arrow_up_black_24dp);
}
typedArray.recycle();
// enforces vertical orientation
setOrientation(LinearLayout.VERTICAL);
// default visibility is gone
setVisibility(GONE);
}
private void findViews() {
mTv = findViewById(R.id.expandable_text);
mTv.setOnClickListener(this);
mButton = findViewById(R.id.expand_collapse);
mButton.setImageDrawable(mCollapsed ? mExpandDrawable : mCollapseDrawable);
mButton.setOnClickListener(this);
}
private static boolean isPostHoneycomb() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}
private static boolean isPostLolipop() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static 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);
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) {
Resources resources = context.getResources();
if (isPostLolipop()) {
return resources.getDrawable(resId, context.getTheme());
} else {
return resources.getDrawable(resId);
}
}
private static int getRealTextViewHeight(@NonNull TextView textView) {
int textHeight = textView.getLayout().getLineTop(textView.getLineCount());
int padding = textView.getCompoundPaddingTop() + textView.getCompoundPaddingBottom();
return textHeight + padding;
}
class ExpandCollapseAnimation extends Animation {
private final View mTargetView;
private final int mStartHeight;
private final int mEndHeight;
public 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);
mTv.setMaxHeight(newHeight - mMarginBetweenTxtAndBottom);
if (Float.compare(mAnimAlphaStart, 1.0f) != 0) {
applyAlphaAnimation(mTv, mAnimAlphaStart + interpolatedTime * (1.0f - mAnimAlphaStart));
}
mTargetView.getLayoutParams().height = newHeight;
mTargetView.requestLayout();
}
@Override
public void initialize( int width, int height, int parentWidth, int parentHeight ) {
super.initialize(width, height, parentWidth, parentHeight);
}
@Override
public boolean willChangeBounds( ) {
return true;
}
}
public interface OnExpandStateChangeListener {
/**
* 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);
}
}
3.布局代码:
说明其中lineSpacingExtra代表的是行间距,默认是0,是一个绝对高度值
lineSpacingMultiplier代表行间距倍数,默认是1.0f,是一个相对高度值
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<fule.com.expandabletextview.view.ExpandableTextView
android:id="@+id/tv_left"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
app:animDuration="200"
app:layout_constraintTop_toTopOf="parent"
app:maxCollapsedLines="3">
<TextView
android:id="@+id/expandable_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left"
android:lineSpacingMultiplier="1.3"
android:text="@string/default_string"
android:textColor="#666666"
android:textSize="12sp"
tools:ignore="RtlHardcoded" />
<ImageView
android:id="@+id/expand_collapse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:padding="6dp"
android:src="@drawable/ic_keyboard_arrow_down_black_24dp" />
</fule.com.expandabletextview.view.ExpandableTextView>
<fule.com.expandabletextview.view.ExpandableTextView
android:id="@+id/tv_center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
app:animDuration="200"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:maxCollapsedLines="3">
<TextView
android:id="@+id/expandable_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left"
android:lineSpacingMultiplier="1.3"
android:text="@string/default_string"
android:textColor="#666666"
android:textSize="12sp"
tools:ignore="RtlHardcoded" />
<ImageView
android:id="@+id/expand_collapse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
android:padding="6dp"
android:src="@drawable/ic_keyboard_arrow_down_black_24dp" />
</fule.com.expandabletextview.view.ExpandableTextView>
<fule.com.expandabletextview.view.ExpandableTextView
android:id="@+id/tv_right"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
app:animDuration="200"
app:layout_constraintBottom_toBottomOf="parent"
app:maxCollapsedLines="3">
<TextView
android:id="@+id/expandable_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left"
android:lineSpacingMultiplier="1.3"
android:text="@string/default_string"
android:textColor="#666666"
android:textSize="12sp"
tools:ignore="RtlHardcoded" />
<ImageView
android:id="@+id/expand_collapse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:padding="6dp"
android:src="@drawable/ic_keyboard_arrow_down_black_24dp" />
</fule.com.expandabletextview.view.ExpandableTextView>
4.res下的资源代码:
attrs:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ExpandableTextView">
<attr name="trimLength" format="integer" />
<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"/>
</declare-styleable>
</resources>
小伙伴们有类似需求的可以看看,如果有更好地的方式和建议,欢迎留言。
项目地址如下: