ExpandableTextView 源码逐行精讲:400 行代码如何实现完整展开收起交互
2026/8/24 9:08:02 网站建设 项目流程

ExpandableTextView 源码逐行精讲:400 行代码如何实现完整展开收起交互

【免费下载链接】ExpandableTextViewAndroid TextView that can expand and collapse.项目地址: https://gitcode.com/gh_mirrors/expand/ExpandableTextView

ExpandableTextView 是一款开源的 Android 展开收起文本控件(Expandable TextView),让开发者用一行setText就能给长文本加上"展开 / 收起"按钮和流畅的高度动画。全文约 400 行的 ExpandableTextView.java 核心源码中,隐藏着几个非常实用的 Android 技巧:二次测量判断文本高度、Animation做高度动画、onInterceptTouchEvent防连点。本文将逐段拆解它的实现思路,新手也能轻松读懂。

1. 效果一览:它能做什么

从上面的演示动图可以看到它的完整交互:

  • 文本超过设定行数时,自动截断并显示「展开」按钮;
  • 点击后以 300ms 高度动画展开全文,按钮变为「收起」;
  • 支持在ListView列表项中使用,且每项的展开状态互不干扰。

整个控件对外暴露的能力只有两行核心 API(见 ExpandableTextView.java 第 252~269 行):

public void setText(@Nullable CharSequence text) // 带列表位置参数,用于 ListView 场景 public void setText(CharSequence text, SparseBooleanArray collapsedStatus, int position)

2. 整体结构:一个 LinearLayout 装两个 TextView

控件继承自LinearLayout(强制竖直方向),内部通过<merge>布局装了两个TextView(布局文件:expandabletextview/src/main/res/layout/expandabletextview.xml):

成员变量角色
mTv显示正文内容的 TextView
mStateTv显示「展开 / 收起」文字 + 图标的按钮
// 构造时直接 inflate 内部布局,强制竖直排列 LayoutInflater.from(context).inflate(R.layout.expandabletextview, this, true); setOrientation(LinearLayout.VERTICAL); setVisibility(GONE); // 默认隐藏,setText 后才显示

💡 细节:setOrientation被重写,传入横向布局会直接抛异常——作者从 API 层面杜绝了误用。

3. 自定义属性速查:11 个 XML 属性一次配置

所有可配置项都定义在expandabletextview/src/main/res/values/attrs.xml,通过TypedArrayinit()中统一读取并赋默认值:

<me.chensir.expandabletextview.ExpandableTextView app:maxCollapsedLines="4" <!-- 收起时最多显示几行(默认 8) --> app:animDuration="300" <!-- 动画时长(默认 300ms) --> app:contentTextSize="16sp" <!-- 正文字号 --> app:contentTextColor="@color/blue" <!-- 正文颜色 --> app:contentLineSpacingMultiplier="1.2" app:expandDrawable="@drawable/..." <!-- 展开按钮图标 --> app:collapseDrawable="@drawable/..." app:expandText="更多" <!-- 按钮文案 --> app:collapseText="收起" app:expandCollapseTextColor="@color/pink" app:DrawableAndTextGravity="left" <!-- 按钮位置:left/center/right --> />

源码中「取属性 → 判空 → 用默认值兜底」的写法(如R.string.expand_stringR.drawable.ic_expand_more_black_12dp)是自定义 View 处理属性的标准范式,值得模仿。

4. 核心逻辑 ①:onMeasure 如何判断"文本太长"

判断是否需要收起,作者没有用字符串长度估算,而是在onMeasure中做了两次测量(源码 206~246 行):

// 第一次:乐观测量——假设文本完全放得下 mStateTv.setVisibility(View.GONE); mTv.setMaxLines(Integer.MAX_VALUE); super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 行数没超过上限 → 不需要按钮,直接返回 if (mTv.getLineCount() <= mMaxCollapsedLines) { return; } // 确实超长:记下完整文本高度,再收起测量一次 mTextHeightWithMaxLines = getRealTextViewHeight(mTv); if (mCollapsed) { mTv.setMaxLines(mMaxCollapsedLines); } mStateTv.setVisibility(View.VISIBLE); super.onMeasure(widthMeasureSpec, heightMeasureSpec); mCollapsedHeight = getMeasuredHeight();

这个"先放开、再收起"的测量策略保证了:

  • 短文本不会出现多余的「展开」按钮;
  • mTextHeightWithMaxLines(完整高度)与mCollapsedHeight(收起高度)两个动画所需数值都被精确拿到。

mRelayout标志位配合setText(),确保只有文本变化时才重新执行这套测量,避免无谓开销。

5. 核心逻辑 ②:onClick 中的状态切换

点击正文或按钮都会走到onClick(源码 137~190 行),逻辑清晰分四步:

mCollapsed = !mCollapsed; // 1. 翻转状态 mStateTv.setText(mCollapsed ? mExpandString : mCollapsedString); // 2. 换文案 mStateTv.setCompoundDrawablesWithIntrinsicBounds( mCollapsed ? mExpandDrawable : mCollapseDrawable, null, null, null); // 3. 换图标 // 4. 创建动画:收起 → 目标 mCollapsedHeight;展开 → 目标 = 当前高 + 超出部分 Animation animation = mCollapsed ? new ExpandCollapseAnimation(this, getHeight(), mCollapsedHeight) : new ExpandCollapseAnimation(this, getHeight(), getHeight() + mTextHeightWithMaxLines - mTv.getHeight());

动画结束后的收尾也很讲究:

public void onAnimationEnd(Animation animation) { clearAnimation(); // 清除动画,防止 applyTransformation 被反复调用 mAnimating = false; if (mListener != null) { mListener.onExpandStateChanged(mTv, !mCollapsed); // 回调通知业务层 } }

通过setOnExpandStateChangeListener()注册的回调在动画结束后触发,业务侧可以安全地基于展开状态做埋点或联动。

6. 核心逻辑 ③:ExpandCollapseAnimation 的高度插值

这是全项目最精华的约 30 行——一个只改"高度"的Animation内部类(源码 364~393 行):

protected void applyTransformation(float interpolatedTime, Transformation t) { // 在 startHeight 与 endHeight 之间按插值因子线性计算高度 final int newHeight = (int) ((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight); mTv.setMaxHeight(newHeight - mMarginBetweenTxtAndBottom); // 约束正文可见高度 mTargetView.getLayoutParams().height = newHeight; // 同时压住整个容器 mTargetView.requestLayout(); }

两点设计巧思:

  1. setMaxHeight与容器高度双管齐下:正文用maxHeight裁剪,容器用固定height布局,动画过程中布局不跳动、不遮挡下方控件;
  2. willChangeBounds()返回true:明确告诉系统动画会改变视图尺寸,让父布局正确响应。

注意:这里的"动画"本质是高度渐变 + 逐帧重布局,所以性能关键在requestLayout()的开销。默认 300ms 的时长就是流畅性与开销的平衡点。

7. 实战细节三连:防连点、列表状态、空文本

源码里还有三处容易被忽略的健壮性设计:

  • 动画期间拦截所有触摸——避免连点导致动画叠加错乱:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mAnimating; // 动画进行中:拦截全部触摸事件 }
  • ListView 状态保持——setText(text, collapsedStatus, position)重载把每项的收起状态存进SparseBooleanArray,ViewHolder 复用时仍能还原上次的展开/收起状态(示例用法见sample/src/main/java/me/chensir/expandabletextview/sample/SampleTextListAdapter.java);
  • 空文本自动隐藏——setText中对空文本直接setVisibility(GONE),避免空控件占据布局空间。

8. 三步快速上手:3 行代码接入

  1. expandabletextview模块导入你的 Android 工程;
  2. 布局 XML 中声明控件并配置属性(参考sample/src/main/res/layout/activity_main.xml);
  3. 在 Activity 中设置文本即可(参考sample/src/main/java/me/chensir/expandabletextview/sample/MainActivity.java):
((ExpandableTextView) findViewById(R.id.tv)).setText(longText);

如果想在自己的项目中完整研读这份源码,可以克隆仓库后打开expandabletextview/src/main/java/me/chensir/expandabletextview/ExpandableTextView.java,对照本文的四个小节顺序(结构 → 属性 → 测量 → 动画)阅读,效率最高:

git clone https://link.gitcode.com/i/c916707246c5538c1be63a6a03e32326.git

总结:400 行代码的精髓

技巧位置可复用场景
二次 onMeasure 判断内容溢出第 206 行任何"内容自适应"控件
高度插值 Animation第 364 行展开面板、评论区折叠
onInterceptTouchEvent 防连点第 193 行一切带动画的按钮
SparseBooleanArray 保存列表状态第 258 行ListView 中保持子视图状态

ExpandableTextView 虽然已停止维护,但它的实现模式至今仍是 Android 自定义 View 教学的优质范本——读懂这 400 行,你就掌握了"展开收起交互"背后的全部核心知识。

【免费下载链接】ExpandableTextViewAndroid TextView that can expand and collapse.项目地址: https://gitcode.com/gh_mirrors/expand/ExpandableTextView

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询