1. 程式人生 > >自定義可伸縮展開的TextView

自定義可伸縮展開的TextView

     最近有個需求,使用者簡介下面的文字可以伸縮擴充套件,預設顯示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>

小夥伴們有類似需求的可以看看,如果有更好地的方式和建議,歡迎留言。

專案地址如下: