RecyclerView滚动时卡片尺寸随机错乱问题排查与解决
问题描述
需要动态修改recyclerView card尺寸,同步调整卡片内部textViews的间距,共分三种场景:
- 场景1:金额为空时,RecyclerView卡片内两个TextView紧凑排列,实现代码:
@Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { holder1.amount.setText(String.valueOf(currentItem.getAmount())); ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(constraintLayout_A); constraintSet.connect(R.id.amount,ConstraintSet.START, ConstraintSet.PARENT_ID,ConstraintSet.START,70); constraintSet.connect(R.id.type, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END,70); constraintSet.applyTo(constraintLayout_A); }
- 场景2:金额数值过大时,两个TextView切换为上下两行排列,实现代码:
@Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { holder2.amount.setText(String.valueOf(currentItem.getAmount())); ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(constraintLayout_A); constraintSet.connect(R.id.amount,ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP,0); constraintSet.connect(R.id.amount,ConstraintSet.START, ConstraintSet.PARENT_ID,ConstraintSet.START,0); constraintSet.connect(R.id.amount,ConstraintSet.END, ConstraintSet.PARENT_ID,ConstraintSet.END,0); constraintSet.connect(R.id.amount,ConstraintSet.BOTTOM, R.id.type,ConstraintSet.TOP,0); constraintSet.connect(R.id.type, ConstraintSet.BOTTOM, R.id.info, ConstraintSet.TOP, 0); constraintSet.connect(R.id.type, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START,0); constraintSet.connect(R.id.type, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END,0); constraintSet.connect(R.id.type, ConstraintSet.TOP, R.id.amount, ConstraintSet.BOTTOM,0); constraintSet.connect(R.id.info, ConstraintSet.START,ConstraintSet.PARENT_ID,ConstraintSet.START,0); constraintSet.connect(R.id.info, ConstraintSet.END,ConstraintSet.PARENT_ID,ConstraintSet.END,0); constraintSet.connect(R.id.info, ConstraintSet.TOP, R.id.type,ConstraintSet.BOTTOM,5); constraintSet.applyTo(constraintLayout_A); }
- 场景3:金额为常规值(占2-3个字符位置)时,保持默认布局,实现代码:
@Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { holder3.amount.setText(String.valueOf(currentItem.getAmount())); }
当前故障
上下滚动recyclerView时,不需要修改布局的列表项会被错误套用布局,比如金额为'0'的项会错误显示为两行布局。已知问题和recylerView的Views复用机制有关。
目前只有设置holder.setIsRecyclable(false);能解决问题,但这个方案性能太差。尝试过拆分三种独立Holder解决,问题依然存在,不确定是实现有误还是思路本身有问题,需要确认正确思路和更优实现方案。
补充适配器代码
package com.example.appName.Adapters; public class Recycler_Adapter_2 extends ListAdapter<Item_get, RecyclerView.ViewHolder> implements Filterable { public Recycler_Adapters_items clickListener; private Item_get currentItem; private ConstraintLayout constraintLayout_A; private Context context; private RecHolder holder1BASIC; private RecHolder2 holder2EXTENDED; private RecHolder4 holder4EXTENDED; public Recycler_Adapter_2(String className, RecyclerView recyclerView) { super(DIFF_CALLBACK); this.className = className; ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams(recyclerView.getLayoutParams()); params.setMarginEnd(5); context = recyclerView.getContext(); } private static final DiffUtil.ItemCallback<Item_get> DIFF_CALLBACK = new DiffUtil.ItemCallback<Item_get>() { @Override public boolean areItemsTheSame(@NonNull Item_get oldItem, @NonNull Item_get newItem) { return oldItem.getId() == newItem.getId(); } @Override public boolean areContentsTheSame(@NonNull Item_get oldItem, @NonNull Item_get newItem) { if(oldItem.getBuy_place() != null){ return oldItem.getname_product().equals(newItem.getname_product()) && oldItem.getAmount().equals(newItem.getAmount()) && oldItem.getBuy_place().equals(newItem.getBuy_place()); }else { return oldItem.getname_product().equals(newItem.getname_product()) && oldItem.getAmount().equals(newItem.getAmount()); } } }; public void setOnItemClickListener(Recycler_Adapters_items clickListener){ this.clickListener = clickListener; } class RecHolder extends RecyclerView.ViewHolder { public TextView name_product; public TextView amountBuy; public TextView typePackage; public RecHolder(@NonNull final View itemView, final Recycler_Adapters_items listener) { super(itemView); name_product = itemView.findViewById(R.id.name_field); amountBuy = itemView.findViewById(R.id.amountBuy); typePackage = itemView.findViewById(R.id.typeP); constraintLayout_A = itemView.findViewById(R.id.ConstrainLayoutShop1_A); initAllNeededArrays(); itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(listener != null){ int position = getAbsoluteAdapterPosition(); if(position != RecyclerView.NO_POSITION){ listener.onItemClick(getItem(position), getAbsoluteAdapterPosition()); } } } }); } } class RecHolder2 extends RecyclerView.ViewHolder { public TextView name_product; public TextView amountBuy; public TextView typePackage; public RecHolder2(@NonNull final View itemView, final Recycler_Adapters_items listener) { super(itemView); name_product = itemView.findViewById(R.id.name_field); amountBuy = itemView.findViewById(R.id.amountBuy); typePackage = itemView.findViewById(R.id.typeP); constraintLayout_A = itemView.findViewById(R.id.ConstrainLayoutShop1_A); initAllNeededArrays(); itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(listener != null){ int position = getAbsoluteAdapterPosition(); if(position != RecyclerView.NO_POSITION){ listener.onItemClick(getItem(position), getAbsoluteAdapterPosition()); } } } }); } } class RecHolder4 extends RecyclerView.ViewHolder { public TextView name_product; public TextView amountBuy; public TextView typePackage; public RecHolder4(@NonNull final View itemView, final Recycler_Adapters_items listener) { super(itemView); name_product = itemView.findViewById(R.id.name_field); amountBuy = itemView.findViewById(R.id.amountBuy); typePackage = itemView.findViewById(R.id.typeP); constraintLayout_A = itemView.findViewById(R.id.ConstrainLayoutShop1_A); initAllNeededArrays(); itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(listener != null){ int position = getAbsoluteAdapterPosition(); if(position != RecyclerView.NO_POSITION){ listener.onItemClick(getItem(position), getAbsoluteAdapterPosition()); } } } }); } } @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View v = null; if(className.equals(ShoppList.class.getName())) { v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_template2,parent,false); switch (viewType){ case 0: return new RecHolder(v,clickListener); case 2: return new RecHolder2(v,clickListener); case 4: return new RecHolder4(v,clickListener); } } return new RecHolder(v,clickListener); } @Override public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { switch (holder.getItemViewType()) { case 0: holder1BASIC = (RecHolder) holder; if (className.equals(ShoppList.class.getName())) { currentItem = getItem(position); holder1BASIC.name_product.setText(currentItem.getname_product()); holder1BASIC.amountToBuy.setText(String.valueOf(currentItem.getAmount())); setPackageText0(holder1BASIC, currentItem.getType(), currentItem.getAmount()); } break; case 2: holder2EXTENDED = (RecHolder2) holder; if (className.equals(ShoppList.class.getName())) { currentItem = getItem(position); holder2EXTENDED.name_product.setText(currentItem.getname_product()); holder2EXTENDED.amountToBuy.setText(String.valueOf(currentItem.getAmount())); setPackageText2(holder2EXTENDED, currentItem.getType(), currentItem.getAmount()); } break; case 4: holder4EXTENDED = (RecHolder4) holder; if (className.equals(ShoppList.class.getName())) { currentItem = getItem(position); holder4EXTENDED.name_product.setText(currentItem.getname_product()); holder4EXTENDED.amountToBuy.setText(String.valueOf(currentItem.getAmount())); setPackageText4(holder4EXTENDED, currentItem.getType(), currentItem.getAmount()); } } } public void nextLineText(){ ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(constraintLayout_A); constraintSet.connect(R.id.amount,ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP,0); constraintSet.connect(R.id.amount,ConstraintSet.START, ConstraintSet.PARENT_ID,ConstraintSet.START,0); constraintSet.connect(R.id.amount,ConstraintSet.END, ConstraintSet.PARENT_ID,ConstraintSet.END,0); constraintSet.connect(R.id.amount,ConstraintSet.BOTTOM, R.id.type,ConstraintSet.TOP,0); constraintSet.connect(R.id.type, ConstraintSet.BOTTOM, R.id.info, ConstraintSet.TOP, 0); constraintSet.connect(R.id.type, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START,0); constraintSet.connect(R.id.type, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END,0); constraintSet.connect(R.id.type, ConstraintSet.TOP, R.id.amount, ConstraintSet.BOTTOM,0); constraintSet.connect(R.id.info, ConstraintSet.START,ConstraintSet.PARENT_ID,ConstraintSet.START,0); constraintSet.connect(R.id.info, ConstraintSet.END,ConstraintSet.PARENT_ID,ConstraintSet.END,0); constraintSet.connect(R.id.info, ConstraintSet.TOP, R.id.type,ConstraintSet.BOTTOM,5); constraintSet.applyTo(constraintLayout_A); } public void setPackageText0( RecHolder holder, int packageTypeCode, int amount){ if(packageTypeCode == 1){ if(amount == 0){ holder.typePackage.setText(packTypeTextResources[2]); }else if(amount == 1){ holder.typePackage.setText(packTypeTextResources[0]); }else if(amount >= 2 && amount <= 4){ holder.typePackage.setText(packTypeTextResources[1]); }else if(amount >= 5){ holder.typePackage.setText(packTypeTextResources[2]); } } } public void setPackageText2( RecHolder2 holder2, int packageTypeCode, int amount){ if(packageTypeCode == 1){ if(amount == 0){ holder2.typePackage.setText(packTypeTextResources[2]); changeMarginDistance(); }else if(amount == 1){ holder2.typePackage.setText(packTypeTextResources[0]); changeMarginDistance(); }else if(amount >= 2 && amount <= 4){ holder2.typePackage.setText(packTypeTextResources[1]); changeMarginDistance(); }else if(amount >= 5){ holder2.typePackage.setText(packTypeTextResources[2]); changeMarginDistance(); } }else { holder2.typePackage.setText(packTypeTextResources[12]); changeMarginDistance(); } } public void setPackageText4( RecHolder4 holder4, int packageTypeCode, int amount){ nextLineText(); if(packageTypeCode == 1){ if(amount == 0){ holder4.typePackage.setText(packTypeTextResources[2]); }else if(amount == 1){ holder4.typePackage.setText(packTypeTextResources[0]); }else if(amount >= 2 && amount <= 4){ holder4.typePackage.setText(packTypeTextResources[1]); }else if(amount >= 5){ holder4.typePackage.setText(packTypeTextResources[2]); } }else { holder4.typePackage.setText(packTypeTextResources[12]); } } public Item_get getItemPos(int position){ return getItem(position); } private void initPackageArrays(){ packTypeTextResources = context.getResources().getStringArray(R.array.Package_Type_Diffrent_varieties); } private void initAllNeededArrays(){ initPackageArrays(); } @Override public int getItemViewType(int position) { // return super.getItemViewType(position); Item_get actualItem = getItem(position); if(actualItem.getAmount() == 0 || actualItem.getAmount() == null){ return 2; }else if (actualItem.getAmount() > 999){ return 4; } return 0; } public void changeMarginDistance(){ ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(constraintLayout_A); constraintSet.connect(R.id.amount,ConstraintSet.START, ConstraintSet.PARENT_ID,ConstraintSet.START,70); constraintSet.connect(R.id.type, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END,70); constraintSet.applyTo(constraintLayout_A); } }
以上为精简后的适配器代码,用于辅助说明问题。
问题原因和修复方案
拆分多Holder的思路本身是对的,但现有代码有两个致命错误,直接导致复用混乱:
- 把
constraintLayout_A、holder1BASIC、holder2EXTENDED、holder4EXTENDED定义成了适配器的全局成员变量,而不是每个Holder自己的内部属性。onBindViewHolder执行时会反复覆盖这些全局变量,调用nextLineText()、changeMarginDistance()的时候,操作的根本不是当前绑定项的布局,而是上一次绑定的Holder持有的布局,自然会出现布局错乱。 - 三个Holder虽然类名不同,但
onCreateViewHolder里三个viewType用的是完全一样的布局文件R.layout.recycler_template2,本质上没有做到布局隔离,RecyclerView复用池里不会按期望的类型隔离缓存,还是会出现跨类型复用的问题。
最优修复步骤
第一步:删掉适配器里的全局错误变量
把适配器类里下面这几个全局成员直接删掉:
private ConstraintLayout constraintLayout_A; private RecHolder holder1BASIC; private RecHolder2 holder2EXTENDED; private RecHolder4 holder4EXTENDED;
第二步:把布局操作逻辑收敛到每个Holder内部
每个Holder自己持有自己的constraintLayout_A引用,不要用全局变量。比如修改RecHolder4的逻辑,把nextLineText()变成Holder内部方法,操作自己的布局:
class RecHolder4 extends RecyclerView.ViewHolder { public TextView name_product; public TextView amountBuy; public TextView typePackage; private ConstraintLayout constraintLayout_A; // 每个Holder自己持有布局引用 public RecHolder4(@NonNull final View itemView, final Recycler_Adapters_items listener) { super(itemView); name_product = itemView.findViewById(R.id.name_field); amountBuy = itemView.findViewById(R.id.amountBuy); typePackage = itemView.findViewById(R.id.typeP); constraintLayout_A = itemView.findViewById(R.id.ConstrainLayoutShop1_A); // 删掉initAllNeededArrays,这个逻辑不要放Holder构造里,挪到适配器初始化的时候做 itemView.setOnClickListener(v -> { if(listener != null){ int position = getAbsoluteAdapterPosition(); if(position != RecyclerView.NO_POSITION){ listener.onItemClick(getItem(position), getAbsoluteAdapterPosition()); } } }); } // 布局修改方法直接操作当前Holder自己的布局 public void applyTwoLineLayout(){ ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(constraintLayout_A); constraintSet.connect(R.id.amount,ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP,0);
相关产品推荐
相关产品推荐

