1. 程式人生 > >Android 點選按鈕隱藏/展開 TextView 實現文字摺疊效果

Android 點選按鈕隱藏/展開 TextView 實現文字摺疊效果

這次版本迭代產品提出了一個很常見的需求:列表中的一個 TextView 條目預設展示兩行文字,超過兩行則展示一個 Button,可點選展開閱讀。再次點選將文字摺疊起來。可摺疊的 TextView 網上教程很多,但找不到這種類似的。做這個需求又遇到一些坑,故記錄一下,供後人參考,喜歡就直接 Ctrl + c/v。

效果展示

圖例一.jpg

圖例二.jpg

圖例三.png

一、需求拆分

  1. 文字不滿兩行時,底部的展開按鈕隱藏
  2. 文字超過兩行,底部顯示展開按鈕,點選按鈕文字展開/隱藏
  3. 記錄展開/隱藏的狀態,當該條目滑出螢幕可見範圍或點選其他條目再返回時,展開/隱藏狀態保持不變

前面兩點很容易實現,重點在第三條。它很容易忽視,所以在第三步踩了點坑

二、具體實現

由於該條目出現在列表中,所以我們將其抽取成一個自定義 View ,這樣邏輯比較清晰,更加解耦。

  • 佈局(標題、正文、底部按鈕)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background
="@color/white" android:orientation="vertical">
<!-- 標題--> <cn.keithxiaoy.TitleView android:id="@+id/titleView" android:layout_width="match_parent" android:layout_height="wrap_content"> </cn.keithxiaoy.TitleView> <LinearLayout
android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/white" android:orientation="vertical" android:paddingLeft="12dp" android:paddingRight="12dp" android:paddingTop="12dp">
<!--重點屬性: lineSpacingExtra = 3dp 行間距 3 dp--> <TextView android:id="@+id/apply_school_desc_notice" android:layout_width="match_parent" android:layout_height="wrap_content" android:lineSpacingExtra="3dp" android:padding="@dimen/default_padding" android:textColor="@color/font_dark" android:textSize="@dimen/font_size_normal2"/> </LinearLayout> <!-- 點選展開/隱藏--> <cn.keithxiaoy.LookAllView android:id="@+id/lookAllView" android:layout_width="match_parent" android:layout_height="wrap_content"> </cn.keithxiaoy.LookAllView> </LinearLayout>
  • 重點業務邏輯
    1.測量一行字型的高度
    2.計算出預設顯示兩行字型所需要的高度(一行字型*2 + 行間距 + 誤差值)
    3.這裡預設顯示兩行,我們可以設定 maxLine 屬性為 2 ,如果服務端返回的資料超過兩行,則可以將該 TextView 的 MaxLine 屬性 設定為 20(或者更多,但筆者覺得通知一般顯示不了 20 行,最優做法是動態計算返回文字的高度,但不想把需求複雜化,所以偷懶了)
    4.測量 TextView 時,需要延遲一會兒,否則無法測量出真實的控制元件高度
    5.做一個標誌來記錄 TextView 的展開/收縮狀態,保證控制元件重繪時能夠保持剛才的控制元件展開狀態。(坑就在這裡,剛開始實現的時候,遇到問題就是當滑動列表控制元件不可見再滑動回來,控制元件的狀態沒有儲存。並且動態獲取 TextView 為兩行字型的高度,所以底部 Button 也被隱藏了)

  • 核心程式碼

    /**
     * notice:伺服器返回的文字,字數未知
     */
    public void bindData(String notice) {
        if (!TextUtils.isEmpty(notice)) {
            setVisibility(View.VISIBLE);
            viewHolder.mTitleView.bindData(TitleView.TITLE_SIGNUPNOTICE);
            viewHolder.mTitleView.setVisibility(View.VISIBLE);
             // 這裡估算出一行字型的高度
            int h = StringUtils.getFontHeigh("報名須知", viewHolder.mApplySchoolDescNotice);
            // DipUtils.dip2px(getContext(), 3) 行間距是3dp ------ DipUtils.dip2px(getContext(), 5) 是誤差值
            final int h2 = 2 * h + DipUtils.dip2px(getContext(), 3) + DipUtils.dip2px(getContext(), 5);
            // 不要先設定 TextView 的最大高度為 2 ,否則測量出來的 TextView 控制元件高度都是展示兩行文字的高度
            viewHolder.mApplySchoolDescNotice.setMaxLines(20);
            viewHolder.mApplySchoolDescNotice.setText(notice);
            // 一定要有延遲,給系統測量的時間
            viewHolder.mApplySchoolDescNotice.post(new Runnable() {
                @Override
                public void run() {
                    if (viewHolder.mApplySchoolDescNotice != null && viewHolder.mLookAllView != null) {
                       // 這裡得到控制元件的高度
                       int h4 = viewHolder.mApplySchoolDescNotice.getHeight();
                       // 控制元件的高度 - 上下的 padding 值
                       int h5 = h4 - DipUtils.dip2px(getContext(), 24);
                        if (h5 > h2) {
                            //大於兩行
                            if (!viewHolder.mLookAllView.getIsExpanded()){
                                //如果不是展開的情況
                                viewHolder.mApplySchoolDescNotice.setMaxLines(2);
                            }
                            viewHolder.mLookAllView.bindText(LookAllView.LOOKMORE_NOTICE, viewHolder.mApplySchoolDescNotice);
                            viewHolder.mLookAllView.setMVisible();
                        } else {
                            //小於兩行,底部「 更多 」不顯示
                            viewHolder.mLookAllView.setMGone();
                            viewHolder.mLookAllView.setViewDividerGone();
                        }
                    }

                }
            });


        } else {
            setVisibility(View.GONE);
        }
    }
  • LookAllView 裡面是點選按鈕,TextView 展開/收縮的邏輯‘’
    /**
     * 繫結資料
     *
     * @param type
     * @param NoticeTextView
     */
    public void bindText(int type, TextView NoticeTextView) {
        mType = type;
        mTextView = NoticeTextView;
        if (!mIsExpanded) {
            lineUp.setVisibility(View.VISIBLE);
            mViewDivider.setVisibility(View.GONE);
            tvMore.setText("更多");
            // 更多/收起 旁邊的小箭頭
            tvMore.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.arrow_gray_down, 0);
        }else {
            lineUp.setVisibility(View.VISIBLE);
            mTextView.setMaxLines(20);
            tvMore.setText("收起");
            // 更多/收起 旁邊的小箭頭
            tvMore.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.arrow_gray_up, 0);
        }
    }


    /**
     * 點選底部 Button 的邏輯
     *
     * @param type
     * @param NoticeTextView
     */
        //點選檢視全部xx
        llMore.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                //更多報名須知
                else if (mType == LookAllView.LOOKMORE_NOTICE) {
                    if (null != mTextView && null != tvMore) {
                        if (tvMore.getText().toString().equalsIgnoreCase("收起")) {
                            lineUp.setVisibility(View.VISIBLE);
                            tvMore.setText("更多");
                            tvMore.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.arrow_gray_down, 0);
                            mTextView.setMaxLines(2);
                            mIsExpanded = false;
                        } else {
                            lineUp.setVisibility(View.VISIBLE);
                            mTextView.setMaxLines(20);
                            tvMore.setText("收起");
                            tvMore.setCompoundDrawablesWithIntrinsicBounds(0, 0, R.drawable.arrow_gray_up, 0);
                            mIsExpanded = true;
                        }

                    }
                }
            }
        });

三、 可伸縮 TextView 完整程式碼

/**
 * Created by KeithXiaoY on 17/07/03.
 */
public class SignUpNoticeItemView extends LinearLayout {

    private ViewHolder viewHolder;
    private boolean isExpanded;

    public SignUpNoticeItemView(Context context) {
        super(context);
        init(context);
    }

    public SignUpNoticeItemView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public SignUpNoticeItemView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public SignUpNoticeItemView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context) {
        // 這裡的佈局在上面已經給出了
        LayoutInflater.from(context).inflate(R.layout.layout_signupnotice_itemview, this);
        viewHolder = new ViewHolder(this);
    }

    static class ViewHolder {

        @Bind(R.id.titleView)
        TitleView mTitleView;
        @Bind(R.id.apply_school_desc_notice)
        TextView mApplySchoolDescNotice;
        @Bind(R.id.lookAllView)
        LookAllView mLookAllView;

        public ViewHolder(View view) {
            ButterKnife.bind(this, view);
        }
    }

    /**
     * 詳細的分析已經寫在本文的第二部分了,真的盡力寫的很詳細了
     */
    public void bindData(String notice) {
        if (!TextUtils.isEmpty(notice)) {
            setVisibility(View.VISIBLE);
            viewHolder.mTitleView.bindData(TitleView.TITLE_SIGNUPNOTICE);
            viewHolder.mTitleView.setVisibility(View.VISIBLE);

            int h = StringUtils.getFontHeigh("報名須知", viewHolder.mApplySchoolDescNotice);
            // DipUtils.dip2px(getContext(), 3) 行間距是3dp ------ DipUtils.dip2px(getContext(), 5) 是誤差值
            final int h2 = 2 * h + DipUtils.dip2px(getContext(), 3) + DipUtils.dip2px(getContext(), 5);
            viewHolder.mApplySchoolDescNotice.setMaxLines(20);
            viewHolder.mApplySchoolDescNotice.setText(notice);
            viewHolder.mApplySchoolDescNotice.post(new Runnable() {
                @Override
                public void run() {
                    if (viewHolder.mApplySchoolDescNotice != null && viewHolder.mLookAllView != null) {
                        int h4 = viewHolder.mApplySchoolDescNotice.getHeight();
                       int h5 = h4 - DipUtils.dip2px(getContext(), 24);
                        if (h5 > h2) {
                            //大於兩行
                            if (!viewHolder.mLookAllView.getIsExpanded()){
                                //如果不是展開的情況
                                viewHolder.mApplySchoolDescNotice.setMaxLines(2);
                            }
                            viewHolder.mLookAllView.bindText(LookAllView.LOOKMORE_NOTICE, viewHolder.mApplySchoolDescNotice);
                            viewHolder.mLookAllView.setMVisible();
                        } else {
                            //小於兩行
                            viewHolder.mLookAllView.setMGone();
                            viewHolder.mLookAllView.setViewDividerGone();
                        }
                    }

                }
            });
        } else {
            setVisibility(View.GONE);
        }
    }

}

四、 結語(如果有更好的實現方法,歡迎留言)

具體實現思路已經寫的很清楚了,註釋寫的也特別詳細了。由於頂部「 標題 」 和 底部「 更多 」複用的地方很多,所以也單獨寫了一個自定義 View 進行解耦,程式碼裡有大段和這個需求無關的東西,所以也沒辦法貼出來。寫程式碼主要是思路,剩下的大家就結合自身的需求來寫吧~

本文原創釋出於微信公眾號「keithxiaoy」,程式設計、思維、成長、正能量,關注並回復「程式設計」、「閱讀」、「Java」、「Python」等關鍵字獲取免費學習資料

不要給自己的人生設限