为什么RecyclerView.notifyItemChanged()会创build一个新的ViewHolder,同时使用旧的ViewHolder和新的ViewHolder?

最近我使用RecyclerView并添加一个自定义的标题视图(另一种types的项目视图),并尝试更新数据发生了变化。 发生一些奇怪的事情。 适配器创build一个新的HeaderViewHolder,并使用新的HeaderViewHolder和旧的HeaderViewHolder。

这是样本。

MainActivity.java

public class MainActivity extends ActionBarActivity { private RecyclerView mRecyclerView; private MyAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mRecyclerView = (RecyclerView) findViewById(R.id.list); LinearLayoutManager llm = new LinearLayoutManager(this); llm.setSmoothScrollbarEnabled(true); mRecyclerView.setLayoutManager(llm); mRecyclerView.setAdapter(mAdapter = new MyAdapter(this, genItemList())); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } public void addItems(View view) { mAdapter.addItemList(genItemList()); } private List<Item> genItemList() { List<Item> list = new ArrayList<>(50); for (int i = 0; i < 50; i++) { Item item = new Item(); item.text1 = "AAAAAAAAAAAAAAAAAAAAAAAA"; item.text2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; list.add(item); } return list; } public void updateHeader(View view) { mAdapter.updateHeader("Updated header"); } } 

MyAdapter.java

 public class MyAdapter extends RecyclerView.Adapter { private static final String TAG = "MyAdapter"; private static final int TYPE_HEADER = 0; private static final int TYPE_ITEM = 1; private LayoutInflater mInflater; private List<Item> mItemList; private Header mHeader; public MyAdapter(Context context, List<Item> items) { mInflater = LayoutInflater.from(context); mItemList = items != null ? items : new ArrayList<Item>(); mHeader = new Header(); mHeader.text = "header"; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int type) { switch (type) { case TYPE_HEADER: Log.d(TAG, "create header view holder"); View headerView = mInflater.inflate(android.R.layout.simple_list_item_1, viewGroup, false); return new HeaderViewHolder(headerView); case TYPE_ITEM: View itemView = mInflater.inflate(R.layout.layout_item, viewGroup, false); return new MyViewHolder(itemView); } return null; } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { if (viewHolder instanceof HeaderViewHolder) { Log.d(TAG, "bind header view holder"); TextView textView = (TextView) viewHolder.itemView.findViewById(android.R.id.text1); textView.setText(mHeader.text); Log.d(TAG, "position: " + position + " holder: " + viewHolder + " text: " + mHeader.text); } else if (viewHolder instanceof MyViewHolder) { Item item = mItemList.get(position - 1) ((MyViewHolder) viewHolder).setText1(item.text1); ((MyViewHolder) viewHolder).setText2(item.text2); } } @Override public int getItemCount() { return mItemList == null ? 0 : mItemList.size() + 1; // plus header } @Override public int getItemViewType(int position) { return position == 0 ? TYPE_HEADER : TYPE_ITEM; } public void addItemList(List<Item> list) { if (list != null) { mItemList.addAll(list); notifyDataSetChanged(); } } public void updateHeader(String text) { mHeader.text = text; notifyItemChanged(0); // notifyDataSetChanged(); } static class HeaderViewHolder extends RecyclerView.ViewHolder { public HeaderViewHolder(View itemView) { super(itemView); } } static class MyViewHolder extends RecyclerView.ViewHolder { TextView mTextView1; TextView mTextView2; public MyViewHolder(View itemView) { super(itemView); mTextView1 = (TextView) itemView.findViewById(R.id.text1); mTextView2 = (TextView) itemView.findViewById(R.id.text2); } public void setText1(String text) { mTextView1.setText(text); } public void setText2(String text) { mTextView2.setText(text); } } } 

Header.java

 public class Header { public String text; } 

Item.java

 public class Item { public String text1; public String text2; } 

activity_main.xml中

 <LinearLayout 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" android:orientation="vertical" tools:context=".MainActivity"> <android.support.v7.widget.RecyclerView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> <LinearLayout style="?android:buttonBarStyle" android:layout_width="match_parent" android:layout_height="56dp" android:orientation="horizontal"> <Button android:id="@+id/add" style="?android:buttonBarButtonStyle" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:onClick="addItems" android:text="Add items" /> <Button android:id="@+id/update" style="?android:buttonBarButtonStyle" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:onClick="updateHeader" android:text="update header" /> </LinearLayout> </LinearLayout> 

layout_item.xml

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/text1" android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="marquee" android:maxLines="1" android:textAppearance="?android:textAppearanceLarge" android:textColor="@android:color/black" /> <TextView android:id="@+id/text2" android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="marquee" android:maxLines="1" android:textAppearance="?android:textAppearanceSmall" android:textColor="@android:color/black" /> </LinearLayout> 

然后,当我点击3次“更新标题”时,这里是logcat输出:

 06-05 19:57:50.368 20400-20400/com.imaygou.recyclerupdateitemdemo D/MyAdapter﹕ create header view holder 06-05 19:57:50.369 20400-20400/com.imaygou.recyclerupdateitemdemo D/MyAdapter﹕ bind header view holder 06-05 19:57:50.370 20400-20400/com.imaygou.recyclerupdateitemdemo D/MyAdapter﹕ position: 0 holder: ViewHolder{3f742717 position=0 id=-1, oldPos=-1, pLpos:-1 no parent} text: header 06-05 19:57:54.030 20400-20400/com.imaygou.recyclerupdateitemdemo D/MyAdapter﹕ create header view holder 06-05 19:57:54.031 20400-20400/com.imaygou.recyclerupdateitemdemo D/MyAdapter﹕ bind header view holder 06-05 19:57:54.031 20400-20400/com.imaygou.recyclerupdateitemdemo D/MyAdapter﹕ position: 0 holder: ViewHolder{3ac01621 position=0 id=-1, oldPos=-1, pLpos:-1 no parent} text: Updated header 06-05 19:57:56.938 20400-20400/com.imaygou.recyclerupdateitemdemo D/MyAdapter﹕ bind header view holder 06-05 19:57:56.938 20400-20400/com.imaygou.recyclerupdateitemdemo D/MyAdapter﹕ position: 0 holder: ViewHolder{3f742717 position=0 id=-1, oldPos=-1, pLpos:-1 no parent} text: Updated header 06-05 19:57:59.613 20400-20400/com.imaygou.recyclerupdateitemdemo D/MyAdapter﹕ bind header view holder 06-05 19:57:59.613 20400-20400/com.imaygou.recyclerupdateitemdemo D/MyAdapter﹕ position: 0 holder: ViewHolder{3ac01621 position=0 id=-1, oldPos=-1, pLpos:-1 no parent} text: Updated header 

如果我使用notifyDataSetChanged()而不是notifyItemChanged(0) ,一切工作正常。 没有人更多的ViewHolder。 但为什么?

为什么它会创build一个新的ViewHolder并使用它们?

什么是使用notifyItemChanged(int)的最佳做法?

Solutions Collecting From Web of "为什么RecyclerView.notifyItemChanged()会创build一个新的ViewHolder,同时使用旧的ViewHolder和新的ViewHolder?"

RecyclerView同时使用ViewHolder从旧状态到新的平滑animation。 这是RecyclerView.ItemAnimator的默认行为。

您可以通过将空项目animation传递给RecyclerView来禁用animation:

 listView.setItemAnimator(null); 

以下是您执行的一些问题:

  • getItemCount需要包含头文件在回收站中的所有项目的计数,所以你应该返回mItemList.size() + 1

  • onBindViewHolder()中的位置字段指的是包括标题在内的整个回收站中元素的位置。 所以绑定一个非标题项目,你会做一些像item = mItemList.get(position - 1) – 这不会失败,因为getItemViewType返回一个大于0的数字TYPE_ITEMs

通过这样做,notifyItemChanged应该按预期行事

 ((SimpleItemAnimator) myRecyclerView.getItemAnimator()).setSupportsChangeAnimations(false); 

更清洁的解决scheme(不是animation师的bug,但这是布局pipe理器的一个特点):

 mRecyclerView.setLayoutManager(new GridLayoutManager(this, 5, LinearLayoutManager.VERTICAL, false){ @Override public boolean supportsPredictiveItemAnimations() { return false;//super.supportsPredictiveItemAnimations(); } });