Android ExpandableListView实战:5分钟搞定课程成绩展示页面(附完整代码)
Android ExpandableListView高效开发打造专业级课程成绩展示界面在移动应用开发中数据展示的清晰度和交互体验直接影响用户满意度。对于教育类应用而言课程成绩的层级化展示是个常见需求——需要同时呈现课程概览和详细评分信息。传统做法可能采用多个Activity跳转或笨重的对话框而ExpandableListView提供了更优雅的解决方案。1. 环境准备与基础配置1.1 项目依赖配置确保项目的build.gradle文件中已包含最新支持库dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 }1.2 布局文件结构规划建议采用以下目录结构组织资源文件res/ ├── layout/ │ ├── activity_grade.xml # 主Activity布局 │ ├── item_group.xml # 父项布局 │ └── item_child.xml # 子项布局 └── values/ ├── colors.xml # 颜色资源 └── dimens.xml # 尺寸资源提示提前定义好颜色和尺寸资源有利于后期维护和主题统一2. 核心实现步骤详解2.1 数据模型设计首先创建成绩数据模型类建议使用Kotlin data class简化代码data class CourseGrade( val courseId: String, val courseName: String, val credit: Float, val score: String, val details: ListGradeDetail emptyList() ) data class GradeDetail( val semester: String, val assessmentType: String, val weight: Float, val actualScore: Float )2.2 界面布局实现主布局文件activity_grade.xmlandroidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent ExpandableListView android:idid/gradeListView android:layout_width0dp android:layout_height0dp app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent android:dividernull android:groupIndicatornull/ /androidx.constraintlayout.widget.ConstraintLayout父项布局item_group.xml关键代码LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationhorizontal android:padding16dp TextView android:idid/tvCourseName android:layout_width0dp android:layout_heightwrap_content android:layout_weight3 android:textSize16sp android:textStylebold/ TextView android:idid/tvScore android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 android:gravityend android:textColor?attr/colorPrimary/ /LinearLayout2.3 适配器高效实现使用BaseExpandableListAdapter的子类处理数据绑定class GradeAdapter( private val context: Context, private val courseList: ListCourseGrade ) : BaseExpandableListAdapter() { override fun getGroupCount(): Int courseList.size override fun getChildrenCount(groupPosition: Int): Int { return courseList[groupPosition].details.size } override fun getGroupView( groupPosition: Int, isExpanded: Boolean, convertView: View?, parent: ViewGroup? ): View { val view convertView ?: LayoutInflater.from(context) .inflate(R.layout.item_group, parent, false) val course getGroup(groupPosition) as CourseGrade view.tvCourseName.text course.courseName view.tvScore.text course.score return view } override fun getChildView( groupPosition: Int, childPosition: Int, isLastChild: Boolean, convertView: View?, parent: ViewGroup? ): View { val view convertView ?: LayoutInflater.from(context) .inflate(R.layout.item_child, parent, false) val detail getChild(groupPosition, childPosition) as GradeDetail view.tvSemester.text detail.semester view.tvAssessmentType.text detail.assessmentType view.tvWeight.text 权重: ${detail.weight}% view.tvActualScore.text 得分: ${detail.actualScore} return view } }3. 高级功能扩展3.1 动画效果优化为展开/折叠添加平滑动画gradeListView.setOnGroupClickListener { _, _, groupPosition, _ - if (gradeListView.isGroupExpanded(groupPosition)) { gradeListView.collapseGroup(groupPosition) } else { gradeListView.expandGroup(groupPosition, true) } true }3.2 性能优化技巧实现ViewHolder模式提升滚动性能// 在适配器中添加内部类 private inner class GroupViewHolder(view: View) { val tvCourseName: TextView view.findViewById(R.id.tvCourseName) val tvScore: TextView view.findViewById(R.id.tvScore) } // 修改getGroupView方法 override fun getGroupView(...): View { val holder: GroupViewHolder val view if (convertView null) { val inflateView LayoutInflater.from(context) .inflate(R.layout.item_group, parent, false) holder GroupViewHolder(inflateView) inflateView.tag holder inflateView } else { holder convertView.tag as GroupViewHolder convertView } // 使用holder设置数据... }3.3 样式自定义方案通过主题属性实现夜间模式支持!-- values/colors.xml -- color namegroupItemTextColor#212121/color color namechildItemBgColor#FAFAFA/color !-- values-night/colors.xml -- color namegroupItemTextColor#E0E0E0/color color namechildItemBgColor#424242/color4. 常见问题解决方案4.1 点击事件处理异常正确处理分组和子项的点击事件gradeListView.setOnChildClickListener { _, _, groupPos, childPos, _ - val selectedDetail adapter.getChild(groupPos, childPos) as GradeDetail showDetailDialog(selectedDetail) true } gradeListView.setOnGroupClickListener { _, _, groupPos, _ - // 返回false允许默认展开/折叠行为 false }4.2 数据更新策略使用DiffUtil高效更新数据fun updateData(newData: ListCourseGrade) { val diffResult DiffUtil.calculateDiff( GradeDiffCallback(courseList, newData) ) courseList.clear() courseList.addAll(newData) diffResult.dispatchUpdatesTo(this) } private class GradeDiffCallback( private val oldList: ListCourseGrade, private val newList: ListCourseGrade ) : DiffUtil.Callback() { // 实现必要的方法... }4.3 内存泄漏预防正确处理上下文引用class GradeAdapter( private val context: Context, private val courseList: ListCourseGrade ) : BaseExpandableListAdapter() { private val weakContext WeakReference(context) override fun getGroupView(...): View { val context weakContext.get() ?: return convertView ?: View(parent.context) // 使用安全的context... } }在真实项目中使用时建议结合ViewModel和LiveData管理数据这样可以在配置变更时保持数据一致性同时避免内存泄漏风险。对于特别复杂的数据展示需求可以考虑使用Epoxy或Groupie等更高级的库来构建界面。