欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android 点击按钮隐藏/展开 TextView 实现文本折叠效果

程序员文章站 2022-05-15 16:56:25
...

未经本人授权,不得转载!否则必将维权到底

这次版本迭代产品提出了一个很常见的需求:列表中的一个 TextView 条目默认展示两行文本,超过两行则展示一个 Button,可点击展开阅读。再次点击将文本折叠起来。可折叠的 TextView 网上教程很多,但找不到这种类似的。做这个需求又遇到一些坑,故记录一下,供后人参考,喜欢就直接 Ctrl + c/v。

效果展示

Android 点击按钮隐藏/展开 TextView 实现文本折叠效果

Android 点击按钮隐藏/展开 TextView 实现文本折叠效果

Android 点击按钮隐藏/展开 TextView 实现文本折叠效果

一、需求拆分

  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」等关键字获取免费学习资料

Android 点击按钮隐藏/展开 TextView 实现文本折叠效果