美文网首页viewAndroid developing tips代码分装
Android 点击按钮隐藏/展开 TextView 实现文本折

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

作者: 程序员K哥 | 来源:发表于2017-07-03 22:14 被阅读1608次

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

    这次版本迭代产品提出了一个很常见的需求:列表中的一个 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 进行解耦,代码里有大段和这个需求无关的东西,所以也没办法贴出来。写代码主要是思路,剩下的大家就结合自身的需求来写吧~


    欢迎关注我的微信公众号与我交流,希望与大家共同成长,未来是属于我们的!

    相关文章

      网友评论

      • 0c3d0bd0b7ce:写过类似需求,只需要在代码中修改TextView的maxLines就行了。当隐藏时候设置2,显示时候设置99999。
        程序员K哥:@紫电麒麟_64da 你没出问题吗?我点击进去其他的界面或者滑动出屏幕,使这个条目不可见,再滑回来,如果是超过两行的情况,就出现bug
        0c3d0bd0b7ce:@keithxiaoy 恰好两行不显示,多余两行会显示,展开合并只改变箭头方向
        程序员K哥:@紫电麒麟_64da 如果两行的情况,底部「查看更多你们显示吗」
      • b408b9daab58:想到一个讨巧的办法,通过修改EditText的MaxLines方法实现
        全世界_gl:@KeithXiaoY 额,你那样的问题是因为复用没还原啊,大兄弟
        程序员K哥:@画船听雨眠king 我猜你说的是textView 吧。设置maxLine 属性为2,如果你一开始属性就写死,滑出屏幕再回到这个条目,你会发现textView 底部的查看更多按钮消失了,并且文本只显示两行。原因是你测量textView 的高度永远是两行文本的高度。 这个 bug ,也是我刚开始想讨巧遇到过了
        程序员K哥:@画船听雨眠king EditText?

      本文标题:Android 点击按钮隐藏/展开 TextView 实现文本折

      本文链接:https://www.haomeiwen.com/subject/bilzcxtx.html