You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RecyclerView用ConstraintSet展开错误项问题求助

解决RecyclerView中ConstraintSet展开折叠时的项复用冲突问题

你的问题我太熟悉了——刚用RecyclerView做展开折叠功能时,几乎都会踩这个ViewHolder复用的坑!咱们一步步拆解问题根源,再给你修改后的代码:

问题核心原因

  1. 全局状态共享导致复用混乱:你用了Adapter级别的expanded和mainLayout变量,RecyclerView的ViewHolder是会被复用的(比如滚动后,第一项的ViewHolder会被拿来显示第10项),所有列表项共享同一个状态,点击第一项时改变的是全局状态,等第10项被复用的时候就会错误应用这个状态,所以看起来是第10项展开了。
  2. 布局缺少唯一ID:mainLayout.getId()为-1是因为根ConstraintLayout没有设置唯一ID,ConstraintSet需要通过ID来匹配控件和约束,没有ID的话,复用的时候无法准确识别哪个布局该应用约束,直接导致约束错位。
  3. ConstraintSet重复初始化:你在onCreateViewHolder里每次都clone ConstraintSet,这既浪费性能,也没有必要——因为折叠和展开的布局是固定的,只需要初始化一次就行。

解决方案步骤

1. 给每个列表项单独存储展开状态

要么在你的ProgressData类里加一个isExpanded字段(推荐),要么用SparseBooleanArray来记录每个位置的状态(不想修改数据类时用)。

比如修改ProgressData:

public class ProgressData {
    private String number;
    // 新增展开状态字段
    private boolean isExpanded;

    // 构造方法、原有getter/setter
    public boolean isExpanded() {
        return isExpanded;
    }

    public void setExpanded(boolean expanded) {
        isExpanded = expanded;
    }

    public String getNumber() {
        return number;
    }

    public void setNumber(String number) {
        this.number = number;
    }
}

2. 给Item根布局设置唯一ID

打开你的progress_item.xml,给根ConstraintLayout加上ID:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_root_layout" <!-- 必须加这个唯一ID -->
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <!-- 你的其他控件 -->
</androidx.constraintlayout.widget.ConstraintLayout>

3. 修改Adapter代码,绑定每个项的独立状态

把全局的状态变量去掉,将ConstraintSet初始化移到构造方法,点击监听器绑定到ViewHolder的布局,并使用当前项的状态:

public class ProgressAdapter extends RecyclerView.Adapter<ProgressAdapter.MyViewHolder> {
    private List<ProgressData> progressData;
    private ConstraintSet collapsedConstraintSet;
    private ConstraintSet expandedConstraintSet;
    private LayoutInflater inflater;

    public class MyViewHolder extends RecyclerView.ViewHolder {
        public TextView t1;
        public ConstraintLayout rootLayout; // 根布局移到ViewHolder里

        public MyViewHolder(View itemView) {
            super(itemView);
            t1 = itemView.findViewById(R.id.t1text);
            rootLayout = itemView.findViewById(R.id.item_root_layout); // 用刚才设置的ID
        }
    }

    public ProgressAdapter(List<ProgressData> progressData, Context context) {
        this.progressData = progressData;
        inflater = LayoutInflater.from(context);
        
        // 只初始化一次ConstraintSet,提升性能
        View collapsedView = inflater.inflate(R.layout.progress_item, null);
        collapsedConstraintSet = new ConstraintSet();
        collapsedConstraintSet.clone(collapsedView);

        expandedConstraintSet = new ConstraintSet();
        expandedConstraintSet.clone(context, R.layout.progress_item_expanded);
    }

    @NonNull
    @Override
    public ProgressAdapter.MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View progressView = inflater.inflate(R.layout.progress_item, parent, false);
        return new MyViewHolder(progressView);
    }

    @Override
    public void onBindViewHolder(@NonNull ProgressAdapter.MyViewHolder holder, final int position) {
        ProgressData progress = progressData.get(position);
        holder.t1.setText(progress.getNumber());

        // 先根据当前项的状态应用对应的约束
        if (progress.isExpanded()) {
            expandedConstraintSet.applyTo(holder.rootLayout);
        } else {
            collapsedConstraintSet.applyTo(holder.rootLayout);
        }

        // 设置点击监听器,绑定当前项的状态
        holder.rootLayout.setOnClickListener(v -> {
            // 切换当前项的展开状态
            boolean newExpandedState = !progress.isExpanded();
            progress.setExpanded(newExpandedState);
            
            // 执行过渡动画
            TransitionManager.beginDelayedTransition(holder.rootLayout);
            
            // 应用新状态的约束
            if (newExpandedState) {
                expandedConstraintSet.applyTo(holder.rootLayout);
            } else {
                collapsedConstraintSet.applyTo(holder.rootLayout);
            }
            
            // 只刷新当前项,避免全局刷新
            notifyItemChanged(position);
        });
    }

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

额外说明

  • 如果不想修改ProgressData类,可以用SparseBooleanArray来替代数据类里的isExpanded字段:
    private SparseBooleanArray expandedStates = new SparseBooleanArray();
    
    在onBindViewHolder里获取状态:
    boolean isExpanded = expandedStates.get(position, false);
    
    点击时更新状态:
    boolean newState = !expandedStates.get(position, false);
    expandedStates.put(position, newState);
    
  • 用notifyItemChanged(position)而不是notifyDataSetChanged(),这样只会刷新当前点击的项,性能更好,也不会影响其他项的状态。

内容的提问来源于stack exchange,提问作者IntersectGlasses

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 04:28:13