新闻详情

新闻详情

首页 / 资讯中心 / 详情

ListView自定义Item全攻略:Adapter机制、ViewHolder优化与常见坑

发布时间:2026/9/9 4:42:07来源:尧图网络
ListView自定义Item全攻略:Adapter机制、ViewHolder优化与常见坑
简介面向Qt基础开发者的自定义ListView Item项目核心解决默认列表项样式单一、难以承载图片文本按钮等复杂内容和交互动效的问题。资源演示了如何继承QStandardItem组织自定义数据并重写QStyledItemDelegate的paint()和sizeHint()方法在QListView上还原接近网易云PC客户端的混合元素列表效果。资源包共18个文件约37KB包含3个cpp源文件、3个h头文件、1个ui界面文件、1个pro工程文件、1个qss样式表、2个qrc资源文件与6张jpg图片素材配套工程可直接用Qt Creator打开便于对照学习。已有2137人学习。读者可以从中掌握绘制代理、尺寸计算、鼠标悬停与点击反馈、数据绑定、列表项内布局管理、动画过渡等关键技巧并可替换自备的qss样式和图片资源快速搭建统一风格的桌面列表界面该示例体积小、结构清晰适合学习Qt模型视图框架和做自定义控件开发的开发者参考。1. 自定义Item之前先搞懂Adapter到底在干什么我印象特别深好几年前刚带团队时有个新人跑过来问我为什么我用ListView显示文字列表没问题想显示一个头像、两行文字、一个按钮这种稍微复杂点的布局就必须去写什么Adapter直接在布局文件里画不行吗这个问题问得挺好因为很多人把ListView当成一个容器往里塞View就行了。但ListView的设计逻辑完全不同——它不是一个你可以随便往里丢东西的盒子而是一个高效的复用型控件。它只负责显示不负责保存数据中间牵线的就是Adapter。用大白话讲Adapter就是ListView和数据源之间的翻译官数据源是一个ListBeanListView要的是一个能填充item的View。翻译官要做的事就是回答一个问题给定第position个数据请给我一个能把它展示出来的View。这个回答的动作就是getView()方法。所以如果你对ListView默认的Item样式不满意比如想显示多字段信息、想加按钮、想换背景本质上不是在改ListView而是在重新实现Adapter里的getView()让它返回你自定义的Item布局。这个机制想明白了后面遇到的很多问题都会豁然开朗。比如为什么ListView滚动时Item会跳、为什么会显示错数据、为什么快速滑动会卡——这些问题全部都能从Adapter的getView执行机制上找到答案。这篇文章我把ListView自定义Item的完整链路拆开讲一遍从最基础的三种写法到ViewHolder的优化逻辑再到多类型Item、嵌套事件、快速滑动的坑最后给几个我实际踩过、排查过的真实场景。不管你是刚学Android的新手还是已经写了一阵子但一直在照着抄的开发者这篇应该都有点参考价值。2. 三种自定义Item的写法以及各自的适用边界自定义Item的写法概括起来有三种。很多人到网上搜教程见一个学一个结果代码风格五花八门性能差异也很大。我先把这三种摆在一起对比再说说什么时候用哪种。2.1 方式一XML布局 在getView里findViewById这是最基础、也最容易理解的方式。先在res/layout下新建一个item的XML布局文件比如item_student.xml里面放一个头像ImageView、两个TextView、一个Button。然后在Adapter的getView()里Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView null) { convertView LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_student, parent, false); } TextView nameView convertView.findViewById(R.id.tv_name); TextView scoreView convertView.findViewById(R.id.tv_score); // ... 对控件赋值 return convertView; }这段代码是所有写法的地基。LayoutInflater负责把XML描述翻译成一颗真实的View树findViewById则在已经构造好的View树上按ID查找具体控件。这种写法的优点是直观、易上手对应的场景是Item结构固定、数量不多比如一屏几十条、开发进度紧张需要快速出活。缺点是每次getView进来都要findViewById虽然在convertView复用后性能没有想象中那么差但ListView高速滑动时这个查找动作依然会产生可感知的开销。2.2 方式二ViewHolder模式把控件引用缓存起来ViewHolder是Google官方在性能优化文档里反复推荐的做法。它做的事情很简单既然Item里的控件位置不会变那我第一次构建View的时候就把每个控件引用存进一个小盒子ViewHolder第二次开始直接从小盒子里取不再重复findViewById。static class ViewHolder { TextView nameView; TextView scoreView; } Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { convertView LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_student, parent, false); holder new ViewHolder(); holder.nameView convertView.findViewById(R.id.tv_name); holder.scoreView convertView.findViewById(R.id.tv_score); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } holder.nameView.setText(studentList.get(position).getName()); return convertView; }关键点在于setTag()——它允许我们把任意对象挂到一个View身上。convertView第一次被创建后把holder挂上去下一次这个convertView被复用进来时通过getTag()直接取回holder所有控件引用都是现成的。这是我在绝大多数项目里采用的方案也是我推荐大多数场景使用的方案。它没有引入任何复杂的框架就是一行setTag的巧思但性能收益非常实在不会再因为每一条item都执行多次findViewById而白白损耗CPU时间。实测在数据量大、Item视图层级深比如超过10个控件的场景下滚动流畅度有明显区别。2.3 方式三布局里不可行的场景用代码动态构建View第三种写法是绕开XML完全用Java代码来new View、设置布局参数。这种写法通常出现在Item布局完全由后台接口动态下发比如运营配置的卡片、Item种类非常多且不确定的场景。Override public View getView(int position, View convertView, ViewGroup parent) { LinearLayout itemRoot new LinearLayout(parent.getContext()); itemRoot.setOrientation(LinearLayout.VERTICAL); // 动态创建子控件并addView return itemRoot; }这种方式的缺点很明显代码可读性差、布局修改成本高、和Android的声明式布局理念相悖。我见过一些早期项目因为赶进度大量使用这种方式维护起来非常痛苦。我个人的经验是除非布局确实是运行时才能确定的否则尽量别用这种方式作为主方案。3. 数据模型与布局的对齐是自定义Item最容易出问题的地方很多人自定义Item写不顺表面看是代码问题实际上是数据模型设计这一步就没做好。Adapter的getView本质上做的是把数据填充到视图的动作这个动作的前提是你的数据Bean里有什么字段你的XML布局里放什么控件、控件什么时候显示什么时候隐藏、显示成什么格式都必须在写之前想清楚。举个例子一个学生列表Item数据Bean可能长这样public class StudentBean { private String name; // 姓名 private String className; // 班级 private int score; // 分数 private int avatarRes; // 头像资源ID private boolean isTop; // 是否置顶 // getter / setter ... }布局里至少要有对应的tv_name展示name、tv_class展示className、tv_score展示分数、iv_avatar展示头像、置顶的item可能需要一个不同的背景色或一个小图标。这一步看起来很简单但实际开发中有一个非常高频的坑**Bean字段含义和UI展示逻辑不是一一对应的而是需要翻译和变形。**比如score可能是原始分数UI上要格式化显示为得分95/100isTop可能影响的不只是文字还有整个item的背景和字体加粗。把这些展示逻辑写在getView里当然能做但会让getView变得臃肿且难以复用。我的做法是给Bean增加几个带业务含义的辅助方法比如getScoreDisplay()返回格式化后的字符串getItemBackgroundRes()根据isTop返回不同背景资源ID。getView里只负责调用不负责拼逻辑这样后期改展示规则只需要改Bean或者一个util类不用翻Adapter。有一个面试也常问的点ListView滚动时Item的重用和数据的正确绑定之间容易引发bug。因为convertView复用后View还带着上一次的界面状态。比如一个Item的按钮是已关注/已取消状态、图片是否加载、文字颜色是否特殊——这些状态都要在getView里针对当前position的数据重新赋值不能假设View是干净的。我见过最典型的翻车场景Item里使用了if (condition) 显示某控件 else 隐藏某控件这个逻辑但因为convertView被复用上一个position时该控件是可见的到了下一个position条件不满足了开发者忘了在else分支里执行setVisibility(View.GONE)结果界面上出现一串多出来的控件。所以我在写getView时有个习惯涉及可见性、选中态、背景色这类容易残留状态的属性一律无条件赋值不依赖默认状态。4. 多类型Item一个ListView里塞进两种以上布局的正确姿势实际项目中一个ListView里放多种Item样式太常见了列表头部放一个轮播图、内容区是文字卡片、底部是一个加载更多的进度条或者消息列表里系统通知和普通消息长相完全不同。这种场景如果你还在getView里写一堆if判断然后inflate不同布局虽然也能工作但ListView的复用机制会被你打乱。正确的做法是让Adapter重写另外两个方法Override public int getItemViewType(int position) { return dataList.get(position).getType(); } Override public int getViewTypeCount() { return 3; // 本列表一共有多少种类型的Item }getItemViewType返回的是当前位置的数据属于第几种类型getViewTypeCount告诉ListView你一共有多少种类型。ListView拿到这两个信息后会在复用时为每种类型分别维护一个复用池——相同类型的convertView才能互相复用不同类型的会各管各的。这时getView()也需要改造成按类型分发Override public View getView(int position, View convertView, ViewGroup parent) { int type getItemViewType(position); switch (type) { case TYPE_BANNER: return getBannerView(position, convertView, parent); case TYPE_CONTENT: return getContentView(position, convertView, parent); default: return getLoadingView(position, convertView, parent); } }这种模式下每个类型各自的ViewHolder要分开写不要混在一个类里。关于getViewTypeCount有个历史需要特别注意**这个值在ListView初始化后就不能动态变化而且每种类型的ViewHolder都会被ListView缓存。**如果你的Item类型是动态的——比如根据服务端配置决定有2种还是5种——一定在初始化时就算好一个最大值而不是在数据加载完成后才突然改getViewTypeCount的返回值否则会跑出异常。我在项目的文案配置组件里就因为这个坑崩溃过几次后来统一在Adapter内部维护了一个MAX_TYPE_COUNT常量超出数量的类型一律合并进默认样式从根上杜绝问题。还有个小细节getViewTypeCount()的返回值最好返回一个固定常量不要每次都重新计算。ListView在初始化时会读取一次你重新计算的次数多了反而让框架内部的type校验逻辑变得不可预测。5. 嵌套Button、点击事件和position的老大难问题自定义Item里最常见的交互就是点击整行点击、行内某个按钮点击、行内不同区域点击不同响应。这三个需求都有成熟的实现方式但新手常犯的错误一堆我挑几个重点说。5.1 整行点击与行内控件的冲突ListView默认就支持Item的点击设置OnItemClickListener就行。但一旦Item里放了Button或者CheckBox这类可点击控件你会发现整行点击失灵了——原因是Button要消费触摸事件ListView的Item点击响应被拦截了。解决思路有两个方向一是给行内控件加属性android:focusablefalse——让控件不抢焦点这样Item可以正常响应点击但代价是这些控件自身的点击事件依然能设置用setOnClickListener只是不会触发整个Item的点击。二是完全不依赖OnItemClickListener改在getView里给convertView设置setOnClickListener然后在回调里通过getTag或者直接闭包捕获position来拿到数据。我个人的建议是一个Item内如果只有一个交互按钮优先用方向一代码最简洁如果有多个交互点比如关注按钮 私信按钮 整个卡片点击方向二反而更清晰因为你可以在每个控件上分别绑定不同的回调不必在listener里做一堆if (id ...)的分支判断。5.2 position的经典误用给Item中的Button绑定事件时很多人会这样写holder.btnFollow.setOnClickListener(v - { StudentBean bean studentList.get(position); // 处理关注逻辑 });这段代码在静止状态下没问题但ListView一旦滚动起来这个position值就是过期的。因为convertView复用了holder也被复用了而position是通过onClick触发时才去遍历列表找到的——等等不是position过期是闭包里捕获的position根本没有更新为最新的位置。更准确地讲getView每次重新绑定时position会更新为当前复用的位置但如果点击发生在滚动结束之后某行Item上绑定的还是上一次滚到该位置时的position。实际写的时候有两种规避方案。方案一v.getTag()。在getView里把position或数据对象挂到按钮的Tag上holder.btnFollow.setTag(position); holder.btnFollow.setOnClickListener(v - { int currentPos (int) v.getTag(); StudentBean bean studentList.get(currentPos); });这样点击发生时取到的是这次getView绑定时的准确位置安全性高很多。方案二直接用数据对象本身比如holder.btnFollow.setTag(bean)点击时从Tag取Bean完全不依赖position。我推荐方案二因为列表数据可能在滚动过程中发生过增删position的可靠性远不如直接持有对象引用。5.3 触摸事件嵌套Item内横向滑动与ListView纵向滚动的交锋这个场景在实现左滑删除、右滑置顶这类交互时几乎必踩。ListView本身是一个纵向滚动的Scrollable容器Item内的横向滑动操作天然和它不冲突但真写到代码里就会发现不处理requestDisallowInterceptTouchEvent竖向滑动列表时横向滑动的检测逻辑会在gesture detector里反复竞争结果要么是Item内的横向滑动反应迟钝要么是ListView的滚动偶尔被吞掉。一个我实测有效的基础方案是在Item的根布局里重写onInterceptTouchEvent或者直接给根布局的OnTouchListener返回对应的事件分发结果。具体做法会受到你用的手势库影响但核心原则一致**横竖手势的判定要交给最内层的可滑动控件统一决策一旦确认是横向滑动立即通知ListView不要拦截后续事件。**如果项目里只是做一个简单的左滑删除我更推荐直接用支持该交互的第三方库别手写这套逻辑投入产出比很低。6. 实测踩过的三个坑以及完整的排查链路自定义Item写多了坑是踩不完的。我挑三个影响最典型、也最容易反复出现的把当时的排查过程写出来比直接给结论更有参考价值。6.1 滚动后Item文字错乱convertView复用带来的数据残留现象很简单列表里100条数据前20条显示的图片和文字都正常往下滚动再滚回来某些行的文字对不上了——A行显示的内容出现在B行甚至出现一整行张冠李戴。当时整个排查过程我是这么走的第一步先确认数据源本身没有错。在getView里写一行Log.d(TAG, position - bean.getName())滚动一圈后对比日志和数据List发现日志对应的数据是正常的——说明数据源没毛病问题出在View的绑定环节。第二步检查getView中是否有设置了某个字段但没设置另一个字段的遗漏。我检查了setText、setImageResource、setVisibility这几个主要绑定发现有一处逻辑当score为0时我给tv_score设置了Text但忘了给它设置一个默认的可见性。上一行数据score大于0时把tv_score设成了VISIBLE下一行score等于0时本意是让它显示暂无成绩但由于我在else分支里直接return了没有重新设置可见性复用的View就把上一行的可见性带过来了。第三步找到问题的根因后修改方案很简单收拢逻辑在getView开头就把所有控件的默认状态赋值一遍然后再按数据覆盖。这个习惯我从那之后一直保留到现在。这类问题的排查思路其实可以归纳成一个可复用公式出现滚动后才出现的异常十有八九是convertView复用时某个View属性没有在每个分支中无条件赋值。6.2 快速滑动时的卡顿findViewById之外的真实瓶颈用户反馈列表滑快了很卡但单独看ViewHolder代码确实写了findViewById也不是瓶颈。于是我用系统自带的Profile工具抓了一帧发现耗时集中在了图片解码上——Item里的头像在getView里直接用了BitmapFactory.decodeResource每滚动一行就要重新解码一次大图。这是很多新手的常见误区以为ViewHolder只解决控件查找性能就够了忽略了ListView的流畅度大头其实是耗时的非UI操作放到了主线程。解码一张几MB的png在主线程做再快的手机也会掉帧。后来我把头像加载全部切成了异步图片加载框架一步到位地解决了主线程解码的问题。项目中如果不想引入重量级图片框架也有一个轻量方案给Bean里的图片字段预先把Bitmap压缩到目标尺寸再放在缓存里getView只负责setImageBitmap。但这么做内存风险比较高图片一多就容易OOM不建议长期用。6.3 Item之间的UI状态联动复用时被遗忘的位置第三个坑发生在一个喜欢/取消喜欢的列表里。用户对第5个Item点了喜欢滚动几屏再滚回来发现第23个Item也变成喜欢了更诡异的是再点第5个Item的取消第23个Item也同时取消了。这个问题的本质是Button的选中状态被convertView复用了但我只处理了点击事件没有在getView里为每个position的按钮根据数据重新赋值选中状态。排查链路也比较清晰确认点击事件里数据更新了studentList.get(position).setLiked(!liked)确认notifyDataSetChanged之后getView会被触发重新绑定发现getView里按钮的setChecked或setSelected没有绑定数据字段拿的还是上一次的控件状态修复方式是在getView里每次都用holder.btnLike.setChecked(bean.isLiked())来同步控件的UI状态。这类问题的通用预防思路是UI控件里凡是可被用户直接改变的状态——选中、勾选、展开、颜色——都必须在getView里由数据源单向驱动而不是依赖控件自己保存状态。7. 性能优化收尾getView里的每一微秒都算数自定义Item的终极话题永远是性能。前面讲过的ViewHolder是基础这里再补充四个我在实践中验证过、且经常被忽略的优化点它们在大列表、复杂Item场景下的收益很可观。第一个优化点**inflate第三方布局时第三个参数parent一定不要传null。**官方文档和Android源码中的lint检查都提醒过inflate传null会导致生成的View没有正确的LayoutParams某些情况下在ListView里显示会多出边距、大小异常严重点还会多布局测量一次。传入parent, false能保证inflate出的视图拥有正确的父容器参数。第二个优化点**合并冗余布局层级。**比如一个纯展示型的Item根布局用FrameLayout就够了不要所有Item都用三层LinearLayout套娃。视图层级每多一层measure和draw的开销都会成倍增长。写布局时可以随时打开Layout Inspector或开发者选项的显示布局边界看层级是否合理。ListView的Item层级只要超过三层屏幕上同时显示10个Item多出来的测量负担就非常明显。第三个优化点**避免在getView里做耗时计算。**包括重复new对象、正则表达式、字符串拼接大文本、日期格式化、IO读写。这些应该提前在数据层处理好getView只做把已经准备好的值放到控件上这一件事。比如日期格式直接在Bean里存成格式化好的字符串不要在getView里通过SimpleDateFormat现场format。SimpleDateFormat本身也不是线程安全的放在getView里在主线程调用性能隐患和线程隐患都不小。第四个优化点ListView自身的两个属性值得设置一下。android:scrollbarsnone去掉滚动条可以减少一点绘制开销android:cacheColorHint#00000000或设置成跟背景一致可以避免滚动时背景闪烁。这两个属性对流畅度的提升不算大但成本极低建议顺手改掉。还有一点要知道ListView在处理Item高度不一致的列表时测量成本会显著变高。如果列表里各Item高度相差悬殊有的100dp有的200dpListView为了保证滚动条位置正确会在布局阶段做额外的测量。能固定Item高度的话尽量固定实在固定不了可以把android:layout_height写到item根布局上而不是依赖wrap_content去重新测量。写完这些我再提一嘴我个人的习惯自定义Item这件事本质上是数据绑定视图这件事的微缩模型。把getView当成一个纯函数来写——输入position和数据输出一个正确展示数据的View——而不是在getView里堆状态、堆逻辑大部分坑都能避免。如果你刚接触ListView自定义Item先照着最简单的写法跑通再逐步把ViewHolder、多类型、事件绑定加上去每一步都搞清楚为什么后面写RecyclerView时你也会发现这些思路全部通用。最后再分享一个小技巧如果你在调试时发现某一行Item怎么改都不刷新、感觉改错文件了先检查你改的Adapter是不是真的被ListView设置了再看是不是数据源在notifyDataSetChanged后被替换成了新对象但Adapter内部持有的还是旧引用——这两种情况占了自定义Item不生效问题的一大半。定位方法也简单在getView里打印一行position和当前数据地址跑一遍就知道改没改对地方了。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

测试用例设计Day2:等价类、边界值与场景法实战解析 2026/9/9 6:30:14

测试用例设计Day2:等价类、边界值与场景法实战解析

1. 从“会写用例”到“写好用例”:第一阶段Day2到底在练什么如果你点进这篇内容,大概率正处于测试用例设计的学习爬坡期。昨天还在纠结测试用例的格式、字段、模板长得什么样,今天就开始被等价类、边界值、场景法这些名词砸得晕头转向——没错…

阅读更多 →
AI编程返工率高?用OpenSpec+SuperPowers实现规范驱动开发 2026/9/9 6:30:14

AI编程返工率高?用OpenSpec+SuperPowers实现规范驱动开发

最近接手一个内部工具项目,光需求澄清就花了三周。每次开发前问产品经理,回答都是“就这样差不多”,等代码写出来又发现完全不是那么回事。后来我把工作流切到SDD(Specification-Driven Development,规范驱动编程&…

阅读更多 →
基于深度卷积神经网络(FusionCNN)的遥感图像融合算法实现-FusionCNN 2026/9/9 6:30:14

基于深度卷积神经网络(FusionCNN)的遥感图像融合算法实现-FusionCNN

本项目为前几天收费帮学妹做的一个项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。 一、项目描述 基于深度卷积神经网络(FusionCNN)的遥感图像融合算法实现 基…

阅读更多 →
嵌套类型转换实战:Python、Java与C语言的解码与避坑指南 2026/9/9 6:30:14

嵌套类型转换实战:Python、Java与C语言的解码与避坑指南

做业务开发这些年,有一个问题几乎每次接口联调都会撞上,就是“嵌套类型转换”。说白了,从上游接口拿到的是一坨嵌套的 JSON,Map 套 List、List 套 Map、里面再藏个对象,而我们的代码里需要的是一个结构体、一个类、一个…

阅读更多 →
从wechatpad.zip看zip工具包的正确打开方式:校验、解压与排障 2026/9/9 6:30:14

从wechatpad.zip看zip工具包的正确打开方式:校验、解压与排障

简介:一份可直接运行的微信JSAPI支付实现包,面向Java后端开发者,旨在解决公众号支付与H5支付接入时的配置复杂、调试繁琐等难题。压缩包共111个文件,其中44个jar依赖提供了微信支付SDK和网络通信能力;16个xml用于Sprin…

阅读更多 →
DFlash2:大模型推理显存优化与KV Cache分页调度实践 2026/9/9 6:27:14

DFlash2:大模型推理显存优化与KV Cache分页调度实践

做推理优化这几年,我最常被问的一句话就是:“你们那个显存到底是怎么省下来的?”早先我还会耐心解释半天,后来干脆把内部这套方案的演进过程整理成文。从最早只解决“KV Cache 别爆显存”的 DFlash,到后来补上数据管线…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞