Android 點選按鈕隱藏/展開 TextView 實現文字摺疊效果
這次版本迭代產品提出了一個很常見的需求:列表中的一個 TextView 條目預設展示兩行文字,超過兩行則展示一個 Button,可點選展開閱讀。再次點選將文字摺疊起來。可摺疊的 TextView 網上教程很多,但找不到這種類似的。做這個需求又遇到一些坑,故記錄一下,供後人參考,喜歡就直接 Ctrl + c/v。
效果展示
一、需求拆分
- 文字不滿兩行時,底部的展開按鈕隱藏
- 文字超過兩行,底部顯示展開按鈕,點選按鈕文字展開/隱藏
- 記錄展開/隱藏的狀態,當該條目滑出螢幕可見範圍或點選其他條目再返回時,展開/隱藏狀態保持不變
前面兩點很容易實現,重點在第三條。它很容易忽視,所以在第三步踩了點坑
二、具體實現
由於該條目出現在列表中,所以我們將其抽取成一個自定義 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」等關鍵字獲取免費學習資料