未经本人授权,不得转载!否则必将维权到底
这次版本迭代产品提出了一个很常见的需求:列表中的一个 TextView 条目默认展示两行文本,超过两行则展示一个 Button,可点击展开阅读。再次点击将文本折叠起来。可折叠的 TextView 网上教程很多,但找不到这种类似的。做这个需求又遇到一些坑,故记录一下,供后人参考,喜欢就直接 Ctrl + c/v。
效果展示
图例一.jpg 图例二.jpg 图例三.png一、需求拆分
- 文本不满两行时,底部的展开按钮隐藏
- 文本超过两行,底部显示展开按钮,点击按钮文本展开/隐藏
- 记录展开/隐藏的状态,当该条目滑出屏幕可见范围或点击其他条目再返回时,展开/隐藏状态保持不变
前面两点很容易实现,重点在第三条。它很容易忽视,所以在第三步踩了点坑
二、具体实现
由于该条目出现在列表中,所以我们将其抽取成一个自定义 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 进行解耦,代码里有大段和这个需求无关的东西,所以也没办法贴出来。写代码主要是思路,剩下的大家就结合自身的需求来写吧~
欢迎关注我的微信公众号与我交流,希望与大家共同成长,未来是属于我们的!
网友评论