ExpandableTextView 源码逐行精讲400 行代码如何实现完整展开收起交互【免费下载链接】ExpandableTextViewAndroid TextView that can expand and collapse.项目地址: https://gitcode.com/gh_mirrors/expand/ExpandableTextViewExpandableTextView 是一款开源的 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显示正文内容的 TextViewmStateTv显示「展开 / 收起」文字 图标的按钮// 构造时直接 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通过TypedArray在init()中统一读取并赋默认值me.chensir.expandabletextview.ExpandableTextView app:maxCollapsedLines4 !-- 收起时最多显示几行默认 8 -- app:animDuration300 !-- 动画时长默认 300ms -- app:contentTextSize16sp !-- 正文字号 -- app:contentTextColorcolor/blue !-- 正文颜色 -- app:contentLineSpacingMultiplier1.2 app:expandDrawabledrawable/... !-- 展开按钮图标 -- app:collapseDrawabledrawable/... app:expandText更多 !-- 按钮文案 -- app:collapseText收起 app:expandCollapseTextColorcolor/pink app:DrawableAndTextGravityleft !-- 按钮位置left/center/right -- /源码中「取属性 → 判空 → 用默认值兜底」的写法如R.string.expand_string、R.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(); }两点设计巧思setMaxHeight与容器高度双管齐下正文用maxHeight裁剪容器用固定height布局动画过程中布局不跳动、不遮挡下方控件willChangeBounds()返回true明确告诉系统动画会改变视图尺寸让父布局正确响应。注意这里的动画本质是高度渐变 逐帧重布局所以性能关键在requestLayout()的开销。默认 300ms 的时长就是流畅性与开销的平衡点。7. 实战细节三连防连点、列表状态、空文本源码里还有三处容易被忽略的健壮性设计动画期间拦截所有触摸——避免连点导致动画叠加错乱Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mAnimating; // 动画进行中拦截全部触摸事件 }ListView 状态保持——setText(text, collapsedStatus, position)重载把每项的收起状态存进SparseBooleanArrayViewHolder 复用时仍能还原上次的展开/收起状态示例用法见sample/src/main/java/me/chensir/expandabletextview/sample/SampleTextListAdapter.java空文本自动隐藏——setText中对空文本直接setVisibility(GONE)避免空控件占据布局空间。8. 三步快速上手3 行代码接入将expandabletextview模块导入你的 Android 工程布局 XML 中声明控件并配置属性参考sample/src/main/res/layout/activity_main.xml在 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),仅供参考