ListView的item中button或者checkbox位置错乱问题解决思路

本文深入探讨了Android中ListView数据错乱的问题,特别是在使用ViewHolder模式时。文章详细解释了问题产生的原因,并给出了两种有效的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    2016年11月折腾一个项目,那时候使用ListView中嵌套button,给button添加setText()或者改变button的背景颜色时,在使用viewHolder的过程中,button数据发生错乱

    其实网上在2015年-2016年间就出现这个问题“作为ListView经典问题之一,如果你尝试过自定义ListView的item,在上面带有一个checkbox的话,那么 当你的item数超过了一页的话,就会出现这个问题,下面我们来分析下出现这种问题的原因,以及如何来 解决这个问题!”。当时经验少也不想折腾这个问题,通过简单的代码调试,隐约是viewHolder出现的问题,直接简单暴力viewHolder去掉,不缓存View,这样就不会出现数据错乱问题,当时就是这样把问题给解决掉了。    

   时隔1年半,去了家公司实习一段时间,发现了自己的很多不足。趁还有两个月时间就毕业,做一些笔记,把以前学过的知识做个记录,解决一些以前难题,沉淀一波。为什么会数据错乱呢?想一想,ListView负责把Item绘制出来,item的样式由你配置,item中的数据有你填充,新手尽量不要怀疑是ListView除了bug,想一想是你使用出了问题。简单的道理,把ListView想象成一个容器,不管用户做什么操作,容器只负责装数据,不负责管理操纵数据,数据的展示最终由绘制出来的item展示,假如当你看到数据展示出现问题,请先了解一下是否是你管理操纵数据出现问题。错误的操纵有:在getView方法里面通过控件id和position来设置控件的text和背景颜色,当然如果数据只有一页[一屏幕数据],在使用viewHolder情况下,是不会出现什么问题的,因为数据总共就一页,viewHolder。但是假如数据大于等于2页,这个时候你上下滑动的时候,就会出现数据错乱问题。这时我们知道,只要不引入ViewHolder,就不会出现数据错乱问题,当数据量多的时候必须引入viewHolder。假如我们非要引入ViewHolder呢?

   先来一波ListView中getView的调用机制图。[这个图是从网上找的]

 

        上图中有一个Recycler的东东,平时我们ListView上可见的Item处于内存中[显示在手机可见屏幕区域],而且他的Item则放在 这个Recycler中,第一次加载item时,当前页面中的convertView都为NULL,当滚出屏幕,这是时候 ConvertView不为空,所以新的一项会复用这个convertView!像上图就绘制出来7个convertView[调用LayoutInflate方法解析xml文件,真实绘制的view,显示在屏幕上],ListView中的item并不是绘制的view[顶多就是view对象][因为没有调用LayoutInflate方法解析xml文件,屏幕上看到的只是convertView+原始data]。换个方式,android中绘制View是比较耗费资源的,假如data中有1万条数据,ListView是否存在1万个item,ListView是否也有1万个View对象,就算你绘制出一万个View,屏幕也不能展示一万个View,最多就是先绘制10个[假设屏幕能展示10个item],滑动一些再绘制10个,一直绘制到1万个,但是手机能给你存放一万个View对象吗?这就和后期的app性能优化相关了。感觉这里不好描述.........见谅吧。

       下面是运行后的一些Log图!


    从图中看出,convertView从0-14都为null,说明创建了先后创建绘制了了14个converView。后面不为null,说明了convertView被缓存复用。

        解决思路:

     1:不使用ViewHolder缓存,直接更改,性能差
     2:使用ViewHolder缓存,此时会存在button状态错乱问题,比较典型的是
        《1》:更改list中的原始data
   使用HashMap来存储button的状态,根据状态更改list里面的原始数据,然后在绘制到adapter里面,这             个时候就不会存 在button错乱问题。[建议使用javaBean来存储状态]
        《2》:假设存在一个极端方式调用onItemClick方法,然后在该方法里面更改
           item中包含view的任何数据。当没有滑动时,这种思路产生的效果时可以完全可以,但是滑动之后,又              会变成原始data,不管这种极端方式是否存在,只要滑动之后,还是会加载原始data,那么我们之后做的工       作将毫无意义。那我们为何不直接更改原始data,一了百了,简单省事。

          ListView之checkbox错位问题解决
         listview多线程下载,解决button错乱问题
         Android 解决listview中checkBox错位选择


    学习点:

      复用同一个list_view_item,如果通过控制id来修改控件字体背景,这个时候会导致复用同一个id的button出现数据错乱,所以通过id修改直接pass掉。还有当下拉加载数据时候,因为使用了ViewHolder缓存,势必会导致ListView中数据错乱。既然是更改数据,可以通过记住控件的现有状态+点击状态,反馈到原始数据中,更改原始数据。或者使用专门的数据结构存储状态,根据状态反馈给原始数据乃至更改原始数据这个时候就不会出现绘制错乱问题。我们知道getView中是用来绘制View的,view都是一样的,假设我们能够控制数据的一致性或者有序性,那么填充数据就不会出现数据错乱。

        其实这篇文章最重要的就是这句红色的话,本来不想接触listView这个问题,看了一个“android实现动画框架”的视频后。深受感触,原来android还能这么玩。

   上代码:

        Activity文件:

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by wn on 2018/4/27.
 */

public class ListViewActivity extends Activity implements MyAdapter.Callback,AdapterView.OnItemClickListener{

    //ListView控件
    private ListView mList;
    //ListView数据源
    private List<AppBean> data;
    private MyAdapter adapter ;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.listview_main);
        data = new ArrayList<AppBean>();
        mList = (ListView)findViewById(R.id.mList);

        for(int i = 0; i < 20; i ++){
            AppBean appBean = new AppBean();
            appBean.setAppName("斗鱼APP"+i);
            appBean.setDownloadStatus("安装");
            data.add(appBean);
        }
        adapter = new MyAdapter(data,this);
        mList.setAdapter(adapter);
        mList.setOnItemClickListener(this);
    }

    @Override
    public void click(View view) {
        switch (view.getId()){
            case R.id.mTv:
                int tv = (int) view.getTag(R.id.tv);
                Toast.makeText(ListViewActivity.this,"我是文本"+tv,Toast.LENGTH_SHORT).show();
                break;
            case R.id.mBtn:
                int btn = (int) view.getTag(R.id.btn);
                Toast.makeText(ListViewActivity.this,"我是按钮"+btn,Toast.LENGTH_SHORT).show();
                if (data.get(btn).getDownloadStatus().equals("已安装")){
                    data.get(btn).setDownloadStatus("安装");
                }else if(data.get(btn).getDownloadStatus().equals("安装")){
                    data.get(btn).setDownloadStatus("已安装");
                }
                updateItem(btn);
                //adapter.notifyDataSetChanged();  //更新全部
                break;
        }
    }

    //listView局部更新
    private void updateItem(int position) {
        //屏幕中第一个可见的条目位置
        int firstVisiblePosition = mList.getFirstVisiblePosition();
        //屏幕中最后一个可见的条目位置
        int lastVisiblePosition = mList.getLastVisiblePosition();
        //在看见范围内才更新,不可见的滑动后自动会调用getView方法更新
        if (position >= firstVisiblePosition && position <= lastVisiblePosition) {
            //获取指定位置view对象
            View view = mList.getChildAt(position - firstVisiblePosition);
            adapter.getView(position, view, mList);
        }

    }

    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
        Toast.makeText(ListViewActivity.this,"我是条目"+i,Toast.LENGTH_SHORT).show();
    }
}

Adapter文件:

import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import java.util.List;

/**
 * Created by wn on 2018/4/27.
 */

public class MyAdapter extends BaseAdapter implements View.OnClickListener {

    //上下文
    private Context context;
    //数据项
    private List<AppBean> data;
    private Callback mCallback;
    public MyAdapter(List<AppBean> data,Callback callback){
        this.data = data;
        mCallback = callback;
    }

    @Override
    public int getCount() {
        return data == null ? 0 : data.size();
    }

    @Override
    public Object getItem(int position) {
        return data.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }


    @Override
    public View getView(int position, View convertView, ViewGroup viewGroup) {
        System.out.println("position:"+position+"convertView:"+convertView);
        ViewHolder viewHolder = null;
        if(context == null) {
            context = viewGroup.getContext();
        }
        if(convertView == null){
            convertView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.list_item,null);
            viewHolder = new ViewHolder();
            viewHolder.textView = (TextView)convertView.findViewById(R.id.mTv);
            viewHolder.button = (Button)convertView.findViewById(R.id.mBtn);
            //为convertView设置tag标志
            convertView.setTag(viewHolder);
        }

        //获取viewHolder实例
        viewHolder = (ViewHolder)convertView.getTag();

        //设置数据
        viewHolder.textView.setText(data.get(position).getAppName());
        viewHolder.button.setText(data.get(position).getDownloadStatus());

        //为viewHolder.mTv和viewHolder.mBtn设置tag标记
        viewHolder.textView.setTag(R.id.tv,position);
        viewHolder.button.setTag(R.id.btn,position);

        //设置监听事件
        viewHolder.textView.setOnClickListener(this);
        viewHolder.button.setOnClickListener(this);
        return convertView;
    }

    @Override
    public void onClick(View view) {
        mCallback.click(view);
    }

    static class ViewHolder{
        TextView textView;
        Button button;
    }

    public interface Callback {
         void click(View view);
     }
}

javaBean文件:

import java.io.Serializable;

/**
 * Created by wn on 2018/4/27.
 */

public class AppBean implements Serializable {

    private String appName ;
    private String downloadStatus ;

    public String getAppName() {
        return appName;
    }

    public void setAppName(String appName) {
        this.appName = appName;
    }

    public String getDownloadStatus() {
        return downloadStatus;
    }

    public void setDownloadStatus(String downloadStatus) {
        this.downloadStatus = downloadStatus;
    }
}

listview_main.xml布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="c.test.MainActivity">

    <ListView
        android:id="@+id/mList"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </ListView>
</RelativeLayout>

list_item.xml布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/mTv"
        android:layout_marginTop="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="20sp"
        android:text="text_view"
        />

    <!--  因为一个item布局中有按钮控件的话,按钮会获得焦点,而此时item就获取不到焦点,
         所以点击Item时不能触发其点击事件,如果想让item也有点击事件的话,则设置Button的
         焦点默认为false
     -->
    <Button
        android:id="@+id/mBtn"
        android:focusable="false"
        android:text="button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:visibility="visible"
        />
</RelativeLayout>
values文件夹下的ids.xml文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--
        我们如何判断我们点击的是哪个item中的TextView和Button呢,因为目前的点击时间
        只能判断是TextView点击了,还是Button,或者item被点击了,除了item能知道是哪
        一项被点击了,其他两个却不知道是在哪个item中被点击了,所以我们需要将代码进
        行在此修改。只需要在MyAdapter中修改代码即可。基本思路是,我们可以将每个被点
        击的控件中设置一个标记,通过View中的setTag(int key, Object tag)方法设置即可,
        第一个key必须是一个资源id
    -->
    <item
        name="btn" type="id" >
    </item>

    <item
        name="tv" type="id">
    </item>

</resources>

效果图:

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值