4

I have a RecyclerView that contains expandable items. clicking on an item expands it. The problem is it also expand some other cards, unexpectedly. I checked everything and I couldn't find why is this happening, but I did manage to find out that the clicked item always somehow has the same id as the other expanded item. The error occurs only when the list is big enough, so I think it has something to do with the RecyclerViews functionality. Also using notifyDataSetChanged() works, but it eliminates the animations, and I want the layout to be animated...

this question looks to discuss the same problem I'm facing... but yet I don't know how to solve it.

I couldn't understand why is this happening or how to fix this... below are some images and code to help you understand better, and maybe see if the problem is in the code...

this is the RecyclerView:

enter image description here

An expanded card item looks like this:

enter image description here

Here's my Adapters class:

public class ActiveGoalsAdapter extends RecyclerView.Adapter<ActiveGoalsAdapter.ActiveGoalsViewHolder> {

    private Context context;
    private Cursor cursor;
    private ArrayList<Goal> activeGoals;
    private static boolean[] openedFromParent = new boolean[]{false, true}, editing = new boolean[]{false};

    public ActiveGoalsAdapter(Context context, ArrayList<Goal> activeGoals, Cursor cursor) {
        this.context = context;
        this.activeGoals = activeGoals;
        this.cursor = cursor;
    }

    public class ActiveGoalsViewHolder extends RecyclerView.ViewHolder {

        public LinearLayout shrunkContainer, subGoalsTitleContainer;
        public RelativeLayout expandedContainer, subGoalsRecyclerViewContainer, btnDelete, btnCancel, btnSave;
        public ConstraintLayout editPanel;
        public CustomProgressBar shrunkProgressBar, expandedProgressBar;
        public ImageButton btnExpandShrink, btnEdit, btnBackToParent;
        public TextView title, description;
        public RecyclerView subGoalsRecyclerView;
        public ExtendedEditText nameET, descriptionET;

        public ActiveGoalsViewHolder(@NonNull View itemView) {
            super(itemView);

            shrunkContainer = itemView.findViewById(R.id.shrunk_active_goal_container);
            expandedContainer = itemView.findViewById(R.id.expanded_active_goal_container);
            editPanel = itemView.findViewById(R.id.edit_panel);
            btnExpandShrink = itemView.findViewById(R.id.active_goal_expand_shrink_btn);
            btnEdit = itemView.findViewById(R.id.active_goal_edit_btn);
            btnBackToParent = itemView.findViewById(R.id.active_goal_back_to_parent_btn);
            shrunkProgressBar = itemView.findViewById(R.id.shrunk_active_goal_progress_bar);
            shrunkProgressBar.enableDefaultGradient(true);
            title = itemView.findViewById(R.id.expanded_active_goal_title);
            expandedProgressBar = itemView.findViewById(R.id.expanded_active_goal_progress_bar);
            expandedProgressBar.enableDefaultGradient(true);
            description = itemView.findViewById(R.id.expanded_active_goal_description);
            subGoalsTitleContainer = itemView.findViewById(R.id.expanded_active_goal_sub_goals_title_container);
            subGoalsRecyclerViewContainer = itemView.findViewById(R.id.expanded_active_goal_sub_goals_container);
            subGoalsRecyclerView = itemView.findViewById(R.id.expanded_active_goal_sub_goals_recyclerview);
            nameET = itemView.findViewById(R.id.expanded_active_goal_edit_name_edit_text);
            descriptionET = itemView.findViewById(R.id.expanded_active_goal_edit_description_edit_text);
            btnDelete = itemView.findViewById(R.id.edit_delete_button);
            btnCancel = itemView.findViewById(R.id.edit_cancel_button);
            btnSave = itemView.findViewById(R.id.edit_save_button);

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (expandedContainer.getVisibility() == View.VISIBLE) {
                        shrink();
                    } else {
                        expand();
                    }
                }
            });

        }

        private void expand(){
            TransitionManager.beginDelayedTransition((ViewGroup) itemView.getRootView(), new AutoTransition());
            expandedContainer.setVisibility(View.VISIBLE);
            shrunkProgressBar.setVisibility(View.INVISIBLE);

        }

        private void shrink(){
            TransitionManager.beginDelayedTransition((ViewGroup) itemView.getRootView(), new AutoTransition());
            expandedContainer.setVisibility(View.GONE);
            shrunkProgressBar.setVisibility(View.VISIBLE);
        }

    }

    @NonNull
    @Override
    public ActiveGoalsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(context);
        View view = inflater.inflate(R.layout.active_goal_card, parent, false);
        return new ActiveGoalsViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ActiveGoalsViewHolder holder, int position) {
        if (activeGoals.get(position) == null) {
            return;
        }
        GoalDBHelper db = new GoalDBHelper(context);

        Goal currentGoal = activeGoals.get(position);
        Cursor subGoalsCursor = db.getSubGoalsCursorOf(currentGoal);
        ArrayList<Goal> subGoalsArrayList = db.getSubGoalsArrayListOf(currentGoal);

        String name = currentGoal.getName(),
                description = currentGoal.getDescription(),
                parent = currentGoal.getParentGoal();
        int timeCounted = currentGoal.getTimeCounted(),
                timeEstimated = currentGoal.getTimeEstimated();

        for (Goal subGoal : activeGoals) {
            if (subGoal.getParentGoal().equals(name)) {
                subGoalsArrayList.add(subGoal);
            }
        }

        holder.shrunkProgressBar.setText(name);
        holder.shrunkProgressBar.setProgress((timeCounted * 100 / timeEstimated));
        holder.shrunkProgressBar.setRadius(300.0f);
        holder.expandedProgressBar.setText("");
        holder.expandedProgressBar.setProgress((timeCounted * 100 / timeEstimated));
        holder.expandedProgressBar.setRadius(300.0f);
        holder.title.setText(name);
        holder.description.setText(description);

        if (subGoalsArrayList.size() <= 0) {
            holder.subGoalsTitleContainer.setVisibility(View.GONE);
            holder.subGoalsRecyclerViewContainer.setVisibility(View.GONE);
        } else {
            holder.subGoalsTitleContainer.setVisibility(View.VISIBLE);
            holder.subGoalsRecyclerViewContainer.setVisibility(View.VISIBLE);
            initSubGoalsAdapter(holder.subGoalsRecyclerView, subGoalsArrayList, subGoalsCursor);
        }

        if (openedFromParent[0]) {
            holder.btnBackToParent.setVisibility(View.VISIBLE);
        } else {
            holder.btnBackToParent.setVisibility(View.GONE);
        }

    }

    public void initSubGoalsAdapter(RecyclerView subGoalsRecyclerView, ArrayList<Goal> subGoals, Cursor subGoalsCursor) {
        GoalsAdapter adapter = new GoalsAdapter(context, subGoals, subGoalsCursor);
        final CarouselLayoutManager layoutManager = new CarouselLayoutManager(CarouselLayoutManager.VERTICAL, false);
        layoutManager.setPostLayoutListener((CarouselLayoutManager.PostLayoutListener) new CarouselZoomPostLayoutListener());
        subGoalsRecyclerView.setLayoutManager(layoutManager);
        subGoalsRecyclerView.setHasFixedSize(true);
        subGoalsRecyclerView.setAdapter(adapter);
    }

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

    public void swapCursor(Cursor newCursor) {
        if (cursor != null) {
            cursor.close();
        }

        cursor = newCursor;

        if (newCursor != null) {
            notifyDataSetChanged();
        }
    }
}

Where is the problem? and how should I fix it?

Help would be highly appreciated

Nitzan Daloomy
  • 166
  • 5
  • 24
  • I think we need to see more code. – Kristy Welsh Jan 30 '20 at 14:46
  • I'll add it, but what code do you need? – Nitzan Daloomy Jan 30 '20 at 14:51
  • @KristyWelsh edited the question a bit... is it better? – Nitzan Daloomy Jan 30 '20 at 23:03
  • Hi, first of all do you really have to instantiate the `GoalDBHelper ` every time in the `onBindViewHolder(...)` method? That looks expensive... Try to remove this line `GoalDBHelper db = new GoalDBHelper(context);` and use the dbHelper as member in the adapter – Vall0n Mar 11 '20 at 14:05
  • Try to add a boolean member variable in the `Goal` class that saves if the recycler view item was clicked (expanded) or not. So this flag shall be updated every time the user clicks on it. Then expand or collapse the view according to the flag in the `Goal` class in the `onBindViewHolder(...)` method. – Vall0n Mar 11 '20 at 14:13
  • Ah maybe this line `openedFromParent[0]` should have done it. Try to replace with openedFromParent[position], where position is the given input parameter from the `onBindViewHolder(...)` – Vall0n Mar 11 '20 at 14:14
  • @Vall0n the problem could not relate to the `openedFromParent[0]`, because it's just a flag for another use, the problem existed also without it... I'm not sure how to apply your goal-flagging solution though... I'll try and tell you if it works for me. Tough I believe it won't work, because in my opinion it's related to the fact that both of the expanded items share the same id somehow. Thanks anyway (: – Nitzan Daloomy Mar 11 '20 at 14:45
  • @Vall0n I've updated the question a bit, added something I realized while testing the problem... please give it another look (: – Nitzan Daloomy Mar 11 '20 at 14:55
  • 1
    Hi @IronMan, the `toggleExpanded(...)` method should also used in the `onBindViewHolder(...)` method to restore it's state when scrolled. Therefore you should save the state of a `Goal` object, where the state shows if the card is expanded or not. I hope that helps to understand the approach... – Vall0n Mar 11 '20 at 15:21
  • @Vall0n I'm sorry. actually I'm no longer using this method... forgot to delete it from the question. I will delete it. The adapters class in the question is the updated code I use. and I didn't understand what should I do... /: – Nitzan Daloomy Mar 11 '20 at 15:35
  • 1
    You probably have a problem with the view state, you need to set all state back in onBindViewHolder cause old views are already expanded and are being recycled – Marcos Vasconcelos Mar 11 '20 at 17:41
  • You should think of a ViewHolder as a shared view. It is reused to display the layout for each item. You seem to want to store the state of the item in the ViewHolder itself. But, as @Vall0n and @Marcos Vasconcelos stated, you should store the state of each item according to its position in your Adapter and respond to the state within `onBindViewHolder()` – Sammy T Mar 11 '20 at 17:46
  • @MarcosVasconcelos and how should I do that? – Nitzan Daloomy Mar 11 '20 at 20:04
  • @SammyT How should I do that? – Nitzan Daloomy Mar 11 '20 at 20:04

3 Answers3

5

The problem is that RecyclerView reuses ViewHolders during scrolling. For example on position 10 it can uses ViewHolder from position 2 (let's imagine this item was expanded) and if you don't bind expanded / collapsed state for ViewHolder on position 10 it will have expanded state. So to solve the problem you have to track ViewHolder state and update ViewHolder every onBindViewHolder method calling.

Here is a good answer related to selection in RecyclerView and you will have almost the same logic for expanded / collapsed states.

https://stackoverflow.com/a/28838834/9169701

Alex
  • 888
  • 2
  • 7
  • 21
  • 1
    after seeing the solution in the link and reading the blog I finally had a clue of what should I do. It was a bit hard to apply the solution to my project since it had almost nothing to do with it, it wasn't layout-changes related at all. But eventually, I managed to solve the problem thanks to your solution. So thank you very much! Unless there'll be a simpler solution, I'll give you the bounty (: – Nitzan Daloomy Mar 11 '20 at 23:08
3

I'm not familiar with the utilities you're using for animation. But, you can do something like this to track and update the visibility of your views:

private ArrayList<MyData> dataList;
private ArrayList<boolean> itemStates; // Create a list to store the item states

public MyAdapter(ArrayList<MyData> myData){
    dataList = myData;
    itemStates = new ArrayList<>();

    // Build the default state values for each position
    for(MyData data: dataList){
        itemStates.add(false);
    }
}

@Override
public void onBindViewHolder(MyHolder holder, int position){
    // Whatever you need to do on each item position ...

    final boolean visible = itemStates.get(position);

    // Set the visibility of whichever view you want
    if(visible){
        holder.myView.setVisibility(View.VISIBLE);
    }else{
        holder.myView.setVisibility(View.GONE);
    }

    // Change the visibility after clicked
    holder.itemView.setOnClickListener(new View.OnClickListener(){
        // Use the ViewHolder's getAdapterPosition()
        // to retrieve a reliable position inside the click callback
        int pos = holder.getAdapterPosition();

        if(visible){
            // Play the hide view animation for this position ...
        }else{
            // Play the show view animation for this position ...
        }

        // Set the new item state
        itemStates.set(pos, !visible);

        // Refresh the Adapter after a delay to give your animation time to play
        // (I've used 500 milliseconds here)
        new Handler().postDelayed(new Runnable(){
            @Override
            public void run(){
                notifyDataSetChanged();
            }
        }, 500);
    });
}
Sammy T
  • 1,924
  • 1
  • 13
  • 20
2

You can refer to my code for the solution, maybe it'll help.

final boolean isExpanded = position == currentPosition;
holder.childLayout.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
holder.itemView.setActivated(isExpanded);
Animation slideDown = AnimationUtils.loadAnimation(context, R.anim.slide_down_animation);
holder.childLayout.startAnimation(slideDown);

if (isExpanded)
    currentPosition = position;

holder.parentLayout.setOnClickListener(v -> {
    currentPosition = isExpanded ? -1 : position;
    notifyItemChanged(currentPosition);
    notifyItemChanged(position);
});

Hope this solves your problem.

Edit: currentPosition is a variable which is assigned to -1 and it stores the current position of the item in the recyclerview.

position is the variable of the BindViewHolder

setActivated() is a method defined for view. You can check it here.

childLayout is the layout of the view that is shown after the expansion.

parentLayout is the layout on which you click to expand.

Community
  • 1
  • 1
Piyush Maheswari
  • 635
  • 3
  • 18