RecyclerView多次添加项目

时间:2016-07-13 22:39:54

标签: android android-recyclerview recycler-adapter

我使用RecyclerView CardLayoutCardLayout包含多个组件。为了填写RecyclerView我使用Adapter,但我的问题是,有时当我添加新项目时,该项目会多次添加到RecyclerView中。但这并不总是可以重现的行为(所以它可能与线程有关吗?)。

我不知道原因:/我会很高兴看到每个提示或建议。

以下代码是"几乎"工作TodoManager,唯一的问题是上面解释的问题。

我的MainActivity

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static String fileName = "todos.ser";

    private ArrayList<Todo> todos;
    private RecyclerViewAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);

        updateTodos();
        adapter = new RecyclerViewAdapter(todos, getWindow());

        final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.todos);
        assert recyclerView != null;
        final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(linearLayoutManager);
        recyclerView.setHasFixedSize(true);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        recyclerView.setAdapter(adapter);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        assert fab != null;
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                adapter.addTodo(new Todo(""), todos.size());
                int count = adapter.getItemCount();
                recyclerView.smoothScrollToPosition(count - 1);
            }
        });
    }

    private void updateTodos() {
        todos = new ArrayList<>();
        todos.add(new Todo(""));
    }

    @Override
    public void onStop() {
        super.onStop();

        Log.i(TAG, "save todos=" + todos.size());
        // Save logic
    }
}

Todo

public class Todo implements Serializable {

    // The creationDate is not used at the moment
    private Date creationDate;
    private String description;
    private boolean isChecked;

    public Todo(String description) {
        this.description = description;
        this.creationDate = new Date();
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public boolean isChecked() {
        return isChecked;
    }

    public void setIsChecked(boolean isChecked) {
        this.isChecked = isChecked;
    }

    public Date getCreationDate() {
        return creationDate;
    }
}

RecyclerViewAdapter

public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.TodoViewHolder> {

    private static final String TAG = "RecyclerViewAdapter";

    private final List<Todo> todos;
    private final Window window;

    public RecyclerViewAdapter(List<Todo> todos, Window window) {
        this.todos = todos;
        this.window = window;
    }

    public class TodoViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, CompoundButton.OnCheckedChangeListener, TextWatcher {
        CheckBox cbDone;
        EditText tvDescription;
        FloatingActionButton btnDelete;

        public TodoViewHolder(View itemView) {
            super(itemView);

            cbDone = (CheckBox)itemView.findViewById(R.id.cbDone);
            tvDescription = (EditText) itemView.findViewById(R.id.tvDescription);
            btnDelete = (FloatingActionButton) itemView.findViewById(R.id.btnDelete);

            tvDescription.addTextChangedListener(this);
            cbDone.setOnCheckedChangeListener(this);
            btnDelete.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
            Log.i(TAG, "onClick called: remove todo.");
            removeTodo(getAdapterPosition());
        }

        @Override
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            Log.i(TAG, "onCheckedChanged called: isDone=" + isChecked);
            Todo todo = getTodo(getAdapterPosition());
            todo.setIsChecked(isChecked);
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            // Do nothing
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            // Do nothing
        }

        @Override
        public void afterTextChanged(Editable s) {
            Log.i(TAG, "afterTextChanged called: text=" + s.toString());
            Todo todo = getTodo(getAdapterPosition());
            todo.setDescription(s.toString());
        }
    }

    public void addTodo(Todo todo, int position) {
        if (position < 0 || position > todos.size()) {
            Log.e(TAG, " add: index=" + position);
        } else {
            todos.add(position, todo);
            notifyItemInserted(position);
        }
    }

    public void removeTodo(int position) {
        if (position < 0 || position >= todos.size()) {
            Log.e(TAG, "remove: index=" + position);
        } else {
            todos.remove(position);
            notifyItemRemoved(position);
        }
    }

    public Todo getTodo(int position) {
        Todo todo = null;
        if (position < 0 || position >= todos.size()) {
            Log.e(TAG, "get: index=" + position);
        } else {
            todo = todos.get(position);
        }
        return todo;
    }

    @Override
    public TodoViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.todo_layout, viewGroup, false);
        return new TodoViewHolder(view);
    }

    @Override
    public void onBindViewHolder(TodoViewHolder holder, int position) {
        holder.cbDone.setChecked(todos.get(position).isChecked());
        holder.tvDescription.setText(todos.get(position).getDescription());

        if(holder.tvDescription.requestFocus()) {
            window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
        }
    }

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

main_layout

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/transparent">

    <RelativeLayout
        android:id="@+id/header_layout"
        android:layout_width="match_parent"
        android:layout_height="@dimen/app_bar_height"
        android:background="@color/colorPrimary"
        android:layout_alignParentTop="true">

        <TextView
            android:id="@+id/header"
            android:layout_height="match_parent"
            android:text="@string/todomanager"
            android:textColor="@android:color/white"
            android:layout_width="wrap_content"
            android:layout_alignParentStart="true"
            android:layout_marginStart="@dimen/margin_header_text"
            android:textSize="@dimen/header_text_size"
            android:gravity="center_vertical" />

        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_margin="@dimen/margin_header_button"
            app:backgroundTint="@color/colorAccent"
            android:tint="@android:color/white"
            app:fabSize="mini"
            android:src="@android:drawable/ic_input_add"
            android:layout_alignParentEnd="true" />
    </RelativeLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/todos"
        android:layout_below="@id/header_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical" />

</RelativeLayout> 

CardView的布局:

<android.support.v7.widget.CardView
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/todo"
    android:layout_below="@id/header_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    card_view:cardElevation="5dp"
    android:layout_marginLeft="10dp"
    android:layout_marginRight="10dp"
    android:layout_marginBottom="5dp"
    android:layout_marginTop="5dp">

    <RelativeLayout
        android:layout_width="fill_parent"
        android:layout_height="match_parent">

        <CheckBox
            android:id="@+id/cbDone"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentStart="true"
            android:layout_centerVertical="true" />

        <EditText
            android:id="@+id/tvDescription"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_toStartOf="@+id/btnDelete"
            android:layout_toEndOf="@+id/cbDone"
            android:hint="@string/insert_description"
            android:background="@android:color/transparent"
            android:padding="@dimen/padding_todo_text"
            android:layout_centerVertical="true"
            />

        <android.support.design.widget.FloatingActionButton
            android:id="@+id/btnDelete"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/margin_todo_button"
            app:fabSize="mini"
            app:backgroundTint="@color/colorAccent"
            android:tint="@android:color/white"
            android:src="@android:drawable/ic_menu_delete"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true" />
    </RelativeLayout>
</android.support.v7.widget.CardView>

样式需要以下代码: dimens.xml

<resources>
    <dimen name="app_bar_height">60dp</dimen>
    <dimen name="fab_margin">16dp</dimen>
    <dimen name="text_margin">16dp</dimen>
    <dimen name="margin_header_text">10sp</dimen>
    <dimen name="header_text_size">24sp</dimen>
    <dimen name="margin_header_button">10sp</dimen>
    <dimen name="padding_todo_text">10dp</dimen>
    <dimen name="margin_todo_button">10sp</dimen>
</resources>

colors.xml

<resources>
    <color name="colorPrimary">#212121</color>
    <color name="colorPrimaryDark">#000000</color>
    <color name="colorAccent">#009688</color>
</resources>

strings.xml

<resources>
    <string name="todomanager">Todos</string>
    <string name="insert_description">insert description ...</string>
</resources>

以下屏幕截图显示了错误的行为。左边的一个是正确的,中间的一个是正确的但是当开始滚动时,一些条目在视图中重复。

Correct behavior at the beginning Still correct behavior Here you can see, that some entries are duplicated in the list

经过一些测试后,我发现这些条目是相同的条目,因此当我单击删除一个条目时,所有重复的条目也会消失。并且问题开始白色滚动,只要我有一些条目不在视图中,我滚动显示的条目是重复的。

我还发现它在我添加第12个元素时开始:

添加第12个元素之前的logcat输出:

I/MainActivity: fab.onClick count=[7]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=test5 position=[6]
I/RecyclerViewAdapter: addTodo position=[7] count=[7]
I/MainActivity: fab.onClick count=[8]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=test6 position=[7]
I/RecyclerViewAdapter: addTodo position=[8] count=[8]
I/MainActivity: fab.onClick count=[9]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=test7 position=[8]
I/RecyclerViewAdapter: addTodo position=[9] count=[9]

添加第10个项目时会出现第一个奇怪的行为,因为afterTextChanged次调用不应该存在,GC免费:

I/MainActivity: fab.onClick count=[10]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=test8 position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=test3 position=[4]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[2]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/art: Background partial concurrent mark sweep GC freed 52541(3MB) AllocSpace objects, 1(52KB) LOS objects, 35% free, 28MB/44MB, paused 7.486ms total 62.227ms
I/RecyclerViewAdapter: addTodo position=[10] count=[10]

当添加第11个和第12个项目时,afterTextChanged个调用会爆炸并且奇怪的行为开始了:

I/MainActivity: fab.onClick count=[11]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=test9 position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=test3 position=[4]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[2]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=test7 position=[8]
I/RecyclerViewAdapter: afterTextChanged called: text=test9 position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: addTodo position=[11] count=[11]
I/MainActivity: fab.onClick count=[12]
I/RecyclerViewAdapter: afterTextChanged called: text= position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=t position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=te position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=tes position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test10 position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test5 position=[6]
I/RecyclerViewAdapter: afterTextChanged called: text=test3 position=[4]
I/RecyclerViewAdapter: afterTextChanged called: text=test2 position=[3]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[2]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=test6 position=[7]
I/RecyclerViewAdapter: afterTextChanged called: text=test8 position=[9]
I/RecyclerViewAdapter: afterTextChanged called: text=test9 position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=test10 position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test10 position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test9 position=[10]
I/RecyclerViewAdapter: afterTextChanged called: text=test10 position=[11]
I/RecyclerViewAdapter: afterTextChanged called: text=test4 position=[5]
I/RecyclerViewAdapter: afterTextChanged called: text=test2 position=[3]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[2]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=test1 position=[2]
I/RecyclerViewAdapter: afterTextChanged called: text=training position=[1]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]
I/RecyclerViewAdapter: afterTextChanged called: text=work position=[0]

我发现的另一个奇怪的行为是我的数据是正确的,当我进入编辑模式时,数据看起来是错误的,当我离开编辑模式时,它看起来还不错。

expected entries duplicated entries in edit mode expected entries again

1 个答案:

答案 0 :(得分:1)

问题出在焦点设置的onBindViewHolder方法中,所以当我删除焦点代码时,它会起作用。

@Override
public void onBindViewHolder(TodoViewHolder holder, int position) {
     holder.cbDone.setChecked(todos.get(position).isChecked());
     holder.tvDescription.setText(todos.get(position).getDescription());

     // Remove the following code
     // if(holder.tvDescription.requestFocus()) {
     //    window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
     // }
}