如何解決CheckBox在RecyclerView錯亂的Bug

如何解決CheckBox在RecyclerView錯亂的Bug

問題

某次在寫RecyclerView時, 內裝CheckBox的時候,
滾動RecyclerView會造成CheckBox勾選亂跳。

完整程式碼

以下是我的完整程式碼, 先將問題呈現出來。
這邊是改自於如何使用RecyclerView
 
Gradle部分先加入這行才能使用RecyclerView, 版本記得用android sdk 25
如果你是更低的版本那麼記得調整為屬於你SDK的版本

compile 'com.android.support:recyclerview-v7:25.0.1'

 接著是主要程式區 這邊是給MainActivity.java專用的main_activity.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:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

 在來就是我們的MainActivity.java部分

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ArrayList<Item> myDataset = new ArrayList<>();
        for(int i = 0; i < 100; i++){
            Item item = new Item();
            if(i == 2) {
                item.setCheck(true);
            } else{
                item.setCheck(false);
            }
            item.setText(i+"");
            myDataset.add(item);
        }
        MyAdapter myAdapter = new MyAdapter(myDataset);
        RecyclerView mList = (RecyclerView) findViewById(R.id.list_view);
        final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        mList.setLayoutManager(layoutManager);
        mList.setAdapter(myAdapter);
    }

    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
        private List<Item> mData;

        public class ViewHolder extends RecyclerView.ViewHolder {
            public TextView mTextView;
            public CheckBox mCheckBox;
            public ViewHolder(View v) {
                super(v);
                mTextView = (TextView) v.findViewById(R.id.info_text);
                mCheckBox = (CheckBox) v.findViewById(R.id.info_chcekbox);
            }
        }

        public MyAdapter(List<Item> data) {
            mData = data;
        }

        @Override
        public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View v = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.item, parent, false);
            ViewHolder vh = new ViewHolder(v);
            return vh;
        }

        @Override
        public void onBindViewHolder(final ViewHolder holder, final int position) {
            Item item = mData.get(position);
            holder.mTextView.setText(item.getText());
            holder.mCheckBox.setChecked(item.isCheck());
            holder.mCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
                    holder.mCheckBox.setChecked(b);
                    mData.get(position).setCheck(b);
                }
            });
        }

        @Override
        public int getItemCount() {
            return mData.size();
        }

    }
    private static class Item{
        String text;
        boolean check;

        public String getText() {
            return text;
        }

        public void setText(String text) {
            this.text = text;
        }

        public boolean isCheck() {
            return check;
        }

        public void setCheck(boolean check) {
            this.check = check;
        }
    }
}

而你會發現recyclerView的item會有變紅字, 所以我們補上item的xml部分

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <TextView
        android:layout_marginLeft="20dp"
        android:layout_alignParentLeft="true"
        android:id="@+id/info_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <CheckBox
        android:layout_marginRight="20dp"
        android:layout_alignParentRight="true"
        android:id="@+id/info_chcekbox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

這樣一來就大致都貼上來了,
此時可以執行程式你會看到


有一個選項被打勾惹
原來是我們在初始化的時候所設定的
  

for(int i = 0; i < 100; i++){
    Item item = new Item();
    if(i == 2) {
        item.setCheck(true);
    } else{
        item.setCheck(false);
    }
    item.setText(i+"");
    myDataset.add(item);
}

但是持續滾動你會發現


居然打勾消失了

原來原因出在這段

@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
    Item item = mData.get(position);
    holder.mTextView.setText(item.getText());
    holder.mCheckBox.setChecked(item.isCheck());
    holder.mCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
            holder.mCheckBox.setChecked(b);
            mData.get(position).setCheck(b);
        }
    });
}

這段看起來很正常啊! 有什麼問題呢?
因為我們每次滾動的時候都會再去呼叫onBindViewHolder這個方法,
可是呢, 我們又必須初始化checkbox把資料塞進去,
這時候CompoundButton.OnCheckedChangeListener之前就設定過了以後,
在CheckBox被改變值的時候,
就會被呼叫, 看出問題了嗎?
當我們onCheckedChanged被呼叫的時候,
就會去改變我們的List內的Item物件的值,
所以資料就會跟著錯亂。

解法

那怎麼辦呢?其實很簡單,
因為我們不想要CheckBox設定值的時候就去調整值
而是要使用者去勾選CheckBox的時候才去改變值
所以我們改成使用OnClickListener這個事件
就可以判斷是否由使用者來改變值了。

@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
    Item item = mData.get(position);
    holder.mTextView.setText(item.getText());
    holder.mCheckBox.setChecked(item.isCheck());
    holder.mCheckBox.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            boolean b = ((CheckBox) view).isChecked();
            holder.mCheckBox.setChecked(b);
            mData.get(position).setCheck(b);
        }
    });
}

此時你就會發現一切都恢復正常了,
怎麼滾動都是正常的。