自定義可伸縮展開的TextView
阿新 • • 發佈:2019-02-10
最近有個需求,使用者簡介下面的文字可以伸縮擴充套件,預設顯示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>
小夥伴們有類似需求的可以看看,如果有更好地的方式和建議,歡迎留言。
專案地址如下: