-1

I am trying to make a nested RecyclerView message system that looks like this:

https://i.stack.imgur.com/Vk4Xb.png

How I want to implement this is to indent each child (depth * 16dp). Additionally, I need to be able to make it so that if a user clicks a message, it collapses/expands its children messages.

The messages come in the form of a JSON response that looks like this:

[
    {
        "id":1,
        "parent_id":0,
        "depth":0,
        "text":"This is a top-level message (depth of 0)",
        "children":[
            {
                "id":2,
                "parent_id":1,
                "depth":1,
                "text":"This is a child message (depth of 1)",
                "children":[
                    {
                        "id":3,
                        "parent_id":2,
                        "depth":2,
                        "text":"This is a child message (depth of 2)",
                        "children":[

                        ]
                    }
                ]
            },
            {
                "id":4,
                "parent_id":1,
                "depth":1,
                "text":"This is a child message (depth of 1)",
                "children":[

                ]
            }
        ]
    },
    {
        "id":5,
        "parent_id":0,
        "depth":0,
        "text":"This is a top-level message (depth of 0)",
        "children":[
            {
                "id":6,
                "parent_id":5,
                "depth":1,
                "text":"This is a child message (depth of 1)",
                "children":[

                ]
            }
        ]
    }
]

The JSON is pretty self-explanatory. Each message has a children property that can contain more messages.

My current code outputs the following result:

https://i.stack.imgur.com/lKQW1.png

As you can see, it's not showing any children messages. How can I modify my code to:

  1. Show the children messages and indent them properly
  2. Create a click event so that if a user clicks a message, it hides/shows the children messages

Here is my MainActivity.java:

public class MainActivity extends AppCompatActivity {

    private List<Message> messages;

    private RecyclerView recyclerView;

    private RecyclerView.LayoutManager mLayoutManager;

    private MessageAdapter messageAdapter;

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

        recyclerView = (RecyclerView) findViewById(R.id.recycler_view);

        mLayoutManager = new LinearLayoutManager(this);

        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setItemAnimator(new DefaultItemAnimator());

        // Set the adapter
        messageAdapter = new MessageAdapter(MainActivity.this, messages);
        recyclerView.setAdapter(messageAdapter);
    }
}

And here is my MessageAdapter.java:

public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.ViewHolder> {

    private Context context;

    private List<Message> messages;

    public MessageAdapter(Context context, List<Message> messages) {
        this.context = context;
        this.messages = messages;
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        public Message message;

        public RelativeLayout messageContainer;

        public TextView messageText;

        public ViewHolder(View v) {
            super(v);

            messageContainer = (RelativeLayout) v.findViewById(R.id.messageContainer);

            messageText = (TextView) v.findViewById(R.id.messageText);
        }

        public void setMessage(Message message) {
            this.message = message;
        }
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.message_layout, parent, false);

        ViewHolder viewHolder = new ViewHolder(view);

        return viewHolder;
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        final Message message = messages.get(position);

        holder.setMessage(message);

        holder.message.setText(message.getText());
    }

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

Here is my message_layout.xml:

<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="wrap_content"
    android:id="@+id/messageContainer">

    <TextView
        android:id="@+id/messageText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</RelativeLayout>

And lastly, here is my Message.java model class:

public class Message implements Serializable {

    @SerializedName("id")
    @Expose
    private Integer id;

    @SerializedName("parent_id")
    @Expose
    private Integer parentId;

    @SerializedName("depth")
    @Expose
    private Integer depth;

    @SerializedName("text")
    @Expose
    private String text;

    @SerializedName("children")
    @Expose
    private List<Message> children = null;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getParentId() {
        return parentId;
    }

    public void setParentId(Integer parentId) {
        this.parentId = parentId;
    }

    public Integer getDepth() {
        return depth;
    }

    public void setDepth(Integer depth) {
        this.depth = depth;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public List<Message> getChildren() {
        return children;
    }

    public void setChildren(List<Message> children) {
        this.children = children;
    }

}
l1699997
  • 11
  • 1

1 Answers1

0

In the setMessage of the ViewHolder, you can just apply the padding to RelativeLayout.

public class ViewHolder extends RecyclerView.ViewHolder {
    private final padding;

    public Message message;

    public RelativeLayout messageContainer;

    public TextView messageText;

    public ViewHolder(View v) {
        super(v);

        // Get the padding offset in pixels.
        Resources r = v.getContext().getResources();
        padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, r.getDisplayMetrics());

        messageContainer = (RelativeLayout) v.findViewById(R.id.messageContainer);

        messageText = (TextView) v.findViewById(R.id.messageText);
    }

    public void setMessage(Message message) {
        this.message = message;

        // Set the padding of the relativelayout.
        final depth = this.message.getDepth();
        messageContainer.setPadding(depth * 16, 0, 0, 0); // left, top, right, bottom
    }
}

EDIT:

Actually it would be better to calculate the padding inside the adapter's constructor so you only have to calculate it once, but that's a minor detail.

EDIT 2:

For point 1: You need to flatten out the children so they are in one long continuous list in the adapter. As long as you keep them in order, they will appear in order on the RecyclerView. So you'll need to recursively go down each message and add the children to the Adapter's list.

You can do a simple method in the model to flatten them like this:

public class Message implements Serializable {
   public List<Message> flattenChildren() {
       ArrayList<Message> childs = new ArrayList();
       if(children != null) {
         for (Message m : children) {
            // First add the child then request the flattened children of that child.
            childs.add(m);
            childs.addAll(m.flattenChildren());
         }
       }
       return childs;
   }
}

For point 2: You need to set up a click listener to listen for item clicks. Then, you need to determine how many items to remove when clicked. The logic for this is more complicated so it's going to take more than a simple modification to the adapter. But basically, in your model, create a method called "getTotalNumberOfChildren" which will calculate how many nodes are in the entire tree. like this:

public class Message implements Serializable {
   public int getTotalNumberOfChildren() {
       int number = 0;
       if (children != null) {
          // calculates the number of children I have plus the number of children each child has
          number = children.size();
          for(Message m : children) {
             number += m.getTotalNumberOfChildren();
          }
       }
       return number;
   }
}

Then, whenever the user clicks on a message, remove the number of messages from that index on. If the message is already closed, then re-add them.

Community
  • 1
  • 1
DeeV
  • 35,865
  • 9
  • 108
  • 95
  • Thanks! One problem down, two to go. The other two problems are that (1) currently my code isn't actually showing any children messages, and (2) how do I set up a click event so that when a user clicks on a message it collapses/expands its children messages? – l1699997 Mar 10 '17 at 21:10
  • I edited to show what I would do. It's not trivial, but hopefully it gets you in the right direction. – DeeV Mar 10 '17 at 21:29
  • Wow, thanks! I'm trying to make sense of your `flattenChildren()` method. Why place this method in the adapter and not the main activity where the adapter is set? Why does it return an int? Also, `childs.flattenChildren()` doesn't seem to work. – l1699997 Mar 11 '17 at 00:57
  • @l1699997 because I copy and pasted it and forgot change the signature. – DeeV Mar 11 '17 at 01:00
  • And it should be 'm.flattenChildren()' – DeeV Mar 11 '17 at 01:01
  • Thanks so much for your help! – l1699997 Mar 11 '17 at 01:17