Android implements comment and reply functions in social applications

Android implements comment and reply functions in social applications

In daily Android development, comment and reply functions are one of the requirements we often encounter, and the display of comment and reply lists generally accounts for a large proportion of the functional modules. For companies with frequent requirement changes and iterations, how to quickly develop a secondary interface to adapt to our functional requirements is undoubtedly a higher priority. First, let's take a look at how the comment and reply lists of other social apps are displayed:

Twitter is a world-renowned social platform with hundreds of millions of users. Their comments and replies only display the first level of data (comment data). For other content (reply content), you need to jump to the page to view it. Zhihu is similar. The first picture was found by our designer. He said that we should follow this style and try to display the comments and replies on one page. Well, there is no way. After all, we are front-end developers. The UI depends on the designer's mood, and the data depends on the background's mood. When we see the design, we must think of the solution first: use recyclerview? listview? No, after analyzing its hierarchy, we find that the comments are a list, and the replies in it are another list. Is it possible to use recyclerview or listview nesting? With an uncertain attitude, I immediately went online to check. Sure enough, most of the implementation methods I found were implemented by nesting. Before coming to the company, the comment reply function in one of the projects used nested listview. Although it handled the sliding conflict problem, the effect was not good and it often jammed. So, I must change my mind here.

There are also people online who say that they use custom views to implement it, but I found that most of them do not deal with view reuse, and the development cost is high, so I will not consider it for the time being. What should I do? I accidentally saw the keyword expandable, and it suddenly occurred to me that Google had released an expandable list control a long time ago - ExpandableListView, but I heard that it is relatively old and has some problems. Forget it, I will give it a try and get familiar with the usage of the previous basic controls.

Let’s take a look at the final effect picture first:

This is just a simple rendering, you can improve it based on this. Okay, without further ado, let's take a look at how the effect is implemented. It should not be difficult to see that the entire page uses CoordinatorLayout to achieve the top parallax effect of the details page. At the same time, here I use ExpandableListView to implement multi-level lists, and then solve their nested sliding problems. OK, let's start with ExpandableListView.

ExpandableListView

The official explanation for ExpandableListView is as follows:

A view that shows items in a vertically scrolling two-level list. This differs from the ListView by allowing two levels: groups which can individually be expanded to show its children. The items come from the ExpandableListAdapter associated with this view.

Simply put, ExpandableListView is a secondary list view for vertical scrolling. The difference between ExpandableListView and ListView is that it can implement secondary grouping and bind data and views through ExpandableListAdapter. Let's implement the effect shown above.

Defined in layout

First, we need to declare the ExpandableListView in the xml layout file:

  1. <ExpandableListView
  2. android:id= "@+id/detail_page_lv_comment"  
  3. android:layout_width= "match_parent"  
  4. android:layout_height= "match_parent"  
  5. android:divider= "@null"  
  6. android:layout_marginBottom= "64dp"  
  7. android:listSelector= "@android:color/transparent"  
  8. android:scrollbars= "none" />

There are two issues to be explained here:

  1. ExpandableListView adds a click effect to its items by default. Since the item also contains childItem, after clicking, the entire item will have a click effect. We can cancel the click effect to avoid affecting the user experience. We only need to set the listSelector in the above code.
  2. ExpandableListView has a default divider, which can be hidden via the divider property.

Setting up the Adapter

Just like using listView, we need to set an adapter for ExpandableListView to bind data and view to it. The adapter of ExpandableListView needs to inherit from ExpandableListAdapter. The specific code is as follows:

  1. public class CommentExpandAdapter extends BaseExpandableListAdapter {
  2. private static final String TAG = "CommentExpandAdapter" ;
  3. private List<CommentDetailBean> commentBeanList;
  4. private Context context;
  5. public CommentExpandAdapter(Context context, List<CommentDetailBean> commentBeanList) {
  6. this.context = context;
  7. this.commentBeanList = commentBeanList;
  8. }
  9. @Override
  10. public   int getGroupCount() {
  11. return commentBeanList. size ();
  12. }
  13. @Override
  14. public   int getChildrenCount( int i) {
  15. if(commentBeanList.get(i).getReplyList() == null ){
  16. return 0;
  17. } else {
  18. return commentBeanList.get(i).getReplyList(). size ()>0 ? commentBeanList.get(i).getReplyList(). size ():0;
  19. }
  20. }
  21. @Override
  22. public Object getGroup( int i) {
  23. return commentBeanList.get(i);
  24. }
  25. @Override
  26. public Object getChild( int i, int i1) {
  27. return commentBeanList.get(i).getReplyList().get(i1);
  28. }
  29. @Override
  30. public long getGroupId( int groupPosition) {
  31. return groupPosition;
  32. }
  33. @Override
  34. public long getChildId( int groupPosition, int childPosition) {
  35. return getCombinedChildId(groupPosition, childPosition);
  36. }
  37. @Override
  38. public boolean hasStableIds() {
  39. return   true ;
  40. }
  41. boolean isLike = false ;
  42. @Override
  43. public   View getGroupView(final int groupPosition, boolean isExpand, View convertView, ViewGroup viewGroup) {
  44. final GroupHolder groupHolder;
  45. if(convertView == null ){
  46. convertView = LayoutInflater. from (context).inflate(R.layout.comment_item_layout, viewGroup, false );
  47. groupHolder = new GroupHolder(convertView);
  48. convertView.setTag(groupHolder);
  49. } else {
  50. groupHolder = (GroupHolder) convertView.getTag();
  51. }
  52. Glide. with (context). load (R.drawable.user_other)
  53. .diskCacheStrategy(DiskCacheStrategy.RESULT)
  54. .error(R.mipmap.ic_launcher)
  55. .centerCrop()
  56. . into (groupHolder.logo);
  57. groupHolder.tv_name.setText(commentBeanList.get(groupPosition).getNickName());
  58. groupHolder.tv_time.setText(commentBeanList.get(groupPosition).getCreateDate());
  59. groupHolder.tv_content.setText(commentBeanList.get(groupPosition).getContent());
  60. groupHolder.iv_like.setOnClickListener(new View .OnClickListener() {
  61. @Override
  62. public void onClick( View   view ) {
  63. if(isLike){
  64. isLike = false ;
  65. groupHolder.iv_like.setColorFilter(Color.parseColor( "#aaaaaa" ));
  66. } else {
  67. isLike = true ;
  68. groupHolder.iv_like.setColorFilter(Color.parseColor( "#FF5C5C" ));
  69. }
  70. }
  71. });
  72. return convertView;
  73. }
  74. @Override
  75. public   View getChildView(final int groupPosition, int childPosition, boolean b, View convertView, ViewGroup viewGroup) {
  76. final ChildHolder childHolder;
  77. if(convertView == null ){
  78. convertView = LayoutInflater. from (context).inflate(R.layout.comment_reply_item_layout,viewGroup, false );
  79. childHolder = new ChildHolder(convertView);
  80. convertView.setTag(childHolder);
  81. }
  82. else {
  83. childHolder = (ChildHolder) convertView.getTag();
  84. }
  85. String replyUser = commentBeanList.get(groupPosition).getReplyList().get(childPosition).getNickName();
  86. if(!TextUtils.isEmpty(replyUser)){
  87. childHolder.tv_name.setText(replyUser + ":" );
  88. }
  89. childHolder.tv_content.setText(commentBeanList.get(groupPosition).getReplyList().get(childPosition).getContent());
  90. return convertView;
  91. }
  92. @Override
  93. public boolean isChildSelectable( int i, int i1) {
  94. return   true ;
  95. }
  96. private class GroupHolder{
  97. private CircleImageView logo;
  98. private TextView tv_name, tv_content, tv_time;
  99. private ImageView iv_like;
  100. public GroupHolder( View   view ) {
  101. logo = view .findViewById(R.id.comment_item_logo);
  102. tv_content = view .findViewById(R.id.comment_item_content);
  103. tv_name = view .findViewById(R.id.comment_item_userName);
  104. tv_time = view .findViewById(R.id.comment_item_time);
  105. iv_like = view .findViewById(R.id.comment_item_like);
  106. }
  107. }
  108. private class ChildHolder{
  109. private TextView tv_name, tv_content;
  110. public ChildHolder( View   view ) {
  111. tv_name = (TextView) view .findViewById(R.id.reply_item_user);
  112. tv_content = (TextView) view .findViewById(R.id.reply_item_content);
  113. }
  114. }
  115. }

Generally speaking, after we customize our own ExpandableListAdapter, we need to implement the following methods:

  • The construction method, needless to say, is generally used to initialize data and other operations.
  • getGroupCount, returns the number of group groups, which refers to the number of comments in the current requirement.
  • getChildrenCount, returns the number of children in the group, which here refers to the number of replies corresponding to the current comment.
  • getGroup, returns the actual data of the group, here refers to the current comment data.
  • getChild, returns the actual data of a child in the group, which refers to the reply data of the current comment.
  • getGroupId, returns the group id, usually passing the current group's position to it.
  • getChildId returns the id of a child in the group. Generally, the current position of the child is also passed to it. However, in order to avoid duplication, you can use getCombinedChildId(groupPosition, childPosition); to get the id and return it.
  • hasStableIds, indicating whether the group and sub-options have stable ids, just return true here.
  • isChildSelectable, indicates whether the child in the group can be selected, and returns true here.
  • getGroupView, that is, returns the view of the group. Generally, some data and view binding work is performed here. Generally, for reuse and efficiency, you can customize ViewHolder. The usage is the same as listview, so I won’t say more here.
  • getChildView returns the view of the child item in the group. It is relatively easy to understand. The first parameter is the position of the current group, and the second parameter is the position of the current child.

The data here is simulated data made by myself, but it should be considered a more common format. The general format is as follows:

Generally, our backend will return some data to us through the interface. If you want to view more comments, you need to jump to the "more pages" to view them. Here, for convenience, we only consider loading part of the data.

Use in Activity

Next, we need to display the secondary list of comments and replies in the activity:

  1. private ExpandableListView expandableListView;
  2. private CommentExpandAdapter adapter;
  3. private CommentBean commentBean;
  4. private List<CommentDetailBean> commentsList;
  5. ...
  6. @Override
  7. protected void onCreate(Bundle savedInstanceState) {
  8. super.onCreate(savedInstanceState);
  9. setContentView(R.layout.activity_main);
  10. initView();
  11. }
  12. private void initView() {
  13. expandableListView = findViewById(R.id.detail_page_lv_comment);
  14. initExpandableListView(commentsList);
  15. }
  16. /**
  17. * Initialize the comment and reply list
  18. */
  19. private void initExpandableListView(final List<CommentDetailBean> commentList){
  20. expandableListView.setGroupIndicator( null );
  21. //Expand all replies by default
  22. adapter = new CommentExpandAdapter(this, commentList);
  23. expandableListView.setAdapter(adapter);
  24. for ( int i = 0; i<commentList. size (); i++){
  25. expandableListView.expandGroup(i);
  26. }
  27. expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
  28. @Override
  29. public boolean onGroupClick(ExpandableListView expandableListView, View   view , int groupPosition, long l) {
  30. boolean isExpanded = expandableListView.isGroupExpanded(groupPosition);
  31. Log.e(TAG, "onGroupClick: current comment id>>>" + commentList.get(groupPosition).getId());
  32. // if(isExpanded){
  33. // expandableListView.collapseGroup(groupPosition);
  34. // } else {
  35. // expandableListView.expandGroup(groupPosition, true );
  36. // }
  37. return   true ;
  38. }
  39. });
  40. expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
  41. @Override
  42. public boolean onChildClick(ExpandableListView expandableListView, View   view , int groupPosition, int childPosition, long l) {
  43. Toast.makeText(MainActivity.this, "Clicked reply" , Toast.LENGTH_SHORT).show();
  44. return   false ;
  45. }
  46. });
  47. expandableListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
  48. @Override
  49. public void onGroupExpand( int groupPosition) {
  50. //toast( "Expand the" +groupPosition+ "group" );
  51. }
  52. });
  53. }
  54. /**
  55. * func: Generate test data
  56. * @return comment data
  57. */
  58. private List<CommentDetailBean> generateTestData(){
  59. Gson gson = new Gson();
  60. commentBean = gson.fromJson(testJson, CommentBean.class);
  61. List<CommentDetailBean> commentList = commentBean.getData().getList();
  62. return commentList;
  63. }

Here is a brief explanation of the above code:

By default, ExpandableListView provides us with its own grouping icon(). Under current requirements, we don’t need to display it at all. We can hide it by calling expandableListView.setGroupIndicator(null).

In general, we may need to expand all groups by default, so I can call the expandableListView.expandGroup(i); method through a loop.

ExpandableListView provides us with group and child click events, which can be set by setOnGroupClickListener and setOnChildClickListener respectively. It is worth noting that if we return false in the group click event, the group will be automatically expanded when we click it, but I encountered a problem here. When I return false, there will be an extra comment in the first comment data. Although there are many similar problems found by searching Baidu, they are not solved after all. Finally, I returned true and manually expanded and contracted them by the following code:

  1. if(isExpanded){
  2. expandableListView.collapseGroup(groupPosition);
  3. } else {
  4. expandableListView.expandGroup(groupPosition, true );
  5. }

In addition, we can also use setOnGroupExpandListener and setOnGroupCollapseListener to monitor the group expansion and contraction status of ExpandableListView.

Comment and reply functions

In order to simulate the entire comment and reply function, we also need to manually insert the receipt and refresh the data list. Here I will simply do a simulation, please ignore some UI details.

Insert comment data

Inserting comment data is relatively simple. You only need to insert a piece of data into the list and refresh it:

  1. String commentContent = commentText.getText().toString().trim();
  2. if(!TextUtils.isEmpty(commentContent)){
  3. //commentOnWork(commentContent);
  4. dialog.dismiss();
  5. CommentDetailBean detailBean = new CommentDetailBean( "Xiao Ming" , commentContent, "Just now" );
  6. adapter.addTheCommentData(detailBean);
  7. Toast.makeText(MainActivity.this, "Comment successful" , Toast.LENGTH_SHORT).show();
  8. } else {
  9. Toast.makeText(MainActivity.this, "Comment content cannot be empty" , Toast.LENGTH_SHORT).show();
  10. }

The addTheCommentData method in the adapter is as follows:

  1. public void addTheCommentData(CommentDetailBean commentDetailBean){
  2. if(commentDetailBean!= null ){
  3. commentBeanList.add ( commentDetailBean );
  4. notifyDataSetChanged();
  5. } else {
  6. throw new IllegalArgumentException( "Comment data is empty!" );
  7. }
  8. }

The code is relatively easy to understand, so I won't explain it in detail.

Insert reply data

First, we need to click on a comment and then @ta, so we need to pop up the reply box in the group's click event:

  1. expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
  2. @Override
  3. public boolean onGroupClick(
  4. ExpandableListView expandableListView, View   view , int groupPosition, long l) {
  5. showReplyDialog(groupPosition);
  6. return   true ;
  7. }
  8. });
  9. ......
  10. /**
  11. * func: Pop-up reply box
  12. */
  13. private void showReplyDialog(final int position){
  14. dialog = new BottomSheetDialog(this);
  15. View commentView = LayoutInflater. from (this).inflate(R.layout.comment_dialog_layout, null );
  16. final EditText commentText = (EditText) commentView.findViewById(R.id.dialog_comment_et);
  17. final Button bt_comment = (Button) commentView.findViewById(R.id.dialog_comment_bt);
  18. commentText.setHint( "Reply to " + commentsList.get(position).getNickName() + "'s comment:" );
  19. dialog.setContentView(commentView);
  20. bt_comment.setOnClickListener(new View .OnClickListener() {
  21. @Override
  22. public void onClick( View   view ) {
  23. String replyContent = commentText.getText().toString().trim();
  24. if(!TextUtils.isEmpty(replyContent)){
  25. dialog.dismiss();
  26. ReplyDetailBean detailBean = new ReplyDetailBean( "小红" ,replyContent);
  27. adapter.addTheReplyData(detailBean, position);
  28. Toast.makeText(MainActivity.this, "Reply Successfully" , Toast.LENGTH_SHORT).show();
  29. } else {
  30. Toast.makeText(MainActivity.this, "The reply content cannot be empty" , Toast.LENGTH_SHORT).show();
  31. }
  32. }
  33. });
  34. commentText.addTextChangedListener(new TextWatcher() {
  35. @Override
  36. public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
  37. }
  38. @Override
  39. public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
  40. if(!TextUtils.isEmpty(charSequence) && charSequence.length()>2){
  41. bt_comment.setBackgroundColor(Color.parseColor( "#FFB568" ));
  42. } else {
  43. bt_comment.setBackgroundColor(Color.parseColor( "#D8D8D8" ));
  44. }
  45. }
  46. @Override
  47. public void afterTextChanged(Editable editable) {
  48. }
  49. });
  50. dialog.show();
  51. }

Inserting reply data is similar to inserting comments above. Here is the code in the adapter:

  1. /**
  2. * by moos on 2018/04/20
  3. * func: insert a data after replying successfully
  4. * @param replyDetailBean new reply data
  5. */
  6. public void addTheReplyData(ReplyDetailBean replyDetailBean, int groupPosition){
  7. if(replyDetailBean!= null ){
  8. Log.e(TAG, "addTheReplyData: >>>>It's time to refresh the reply list:" + replyDetailBean.toString() );
  9. if(commentBeanList.get(groupPosition).getReplyList() != null ){
  10. commentBeanList.get(groupPosition).getReplyList(). add (replyDetailBean);
  11. } else {
  12. List<ReplyDetailBean> replyList = new ArrayList<>();
  13. replyList.add (replyDetailBean);
  14. commentBeanList.get(groupPosition).setReplyList(replyList);
  15. }
  16. notifyDataSetChanged();
  17. } else {
  18. throw new IllegalArgumentException( "The reply data is empty!" );
  19. }
  20. }

One thing to note is that not all comments have reply data, so before inserting data we need to determine whether ReplyList is empty. If it is not empty, directly get the reply list of the current comment and insert the data; if it is empty, you need to create a new ReplyList, and after inserting the data, you also need to set ReplyList for the comment.

4. Solve the nesting problem of CoordinatorLayout and ExpandableListView

If you don't need to use CoordinatorLayout or NestedScrollView, you can skip this section. Generally speaking, for a better user experience, our products also require us to add similar top parallax effects or pull-down refresh, etc., which requires us to deal with some common nested sliding problems.

Since CoordinatorLayout implements the NestedScrollingParent interface and RecycleView implements the NestedScrollingChild interface, nested sliding can be achieved with the help of NestedScrollingChildHelper. Then we can also achieve the same effect by implementing the NestedScrollingChild interface through a custom ExpandableListView:

  1. /**
  2. * Desc : Customize ExpandableListView to solve the sliding conflict with CoordinatorLayout
  3. */
  4. public class CommentExpandableListView extends ExpandableListView implements NestedScrollingChild{
  5. private NestedScrollingChildHelper mScrollingChildHelper;
  6. public CommentExpandableListView(Context context, AttributeSet attrs) {
  7. super(context, attrs);
  8. mScrollingChildHelper = new NestedScrollingChildHelper(this);
  9. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  10. setNestedScrollingEnabled( true );
  11. }
  12. }
  13. @Override
  14. protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
  15. int expandSpec = MeasureSpec.makeMeasureSpec( Integer .MAX_VALUE >> 2, MeasureSpec.AT_MOST);
  16. super.onMeasure(widthMeasureSpec, expandSpec);
  17. }
  18. @Override
  19. public void setNestedScrollingEnabled(boolean enabled) {
  20. mScrollingChildHelper.setNestedScrollingEnabled(enabled);
  21. }
  22. @Override
  23. public boolean isNestedScrollingEnabled() {
  24. return mScrollingChildHelper.isNestedScrollingEnabled();
  25. }
  26. @Override
  27. public boolean startNestedScroll( int axes) {
  28. return mScrollingChildHelper.startNestedScroll(axes);
  29. }
  30. @Override
  31. public void stopNestedScroll() {
  32. mScrollingChildHelper.stopNestedScroll();
  33. }
  34. @Override
  35. public boolean hasNestedScrollingParent() {
  36. return mScrollingChildHelper.hasNestedScrollingParent();
  37. }
  38. @Override
  39. public boolean dispatchNestedScroll( int dxConsumed, int dyConsumed, int dxUnconsumed,
  40. int dyUnconsumed, int [] offsetInWindow) {
  41. return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
  42. dxUnconsumed, dyUnconsumed, offsetInWindow);
  43. }
  44. @Override
  45. public boolean dispatchNestedPreScroll( int dx, int dy, int [] consumed, int [] offsetInWindow) {
  46. return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
  47. }
  48. @Override
  49. public boolean dispatchNestedFling( float velocityX, float velocityY, boolean consumed) {
  50. return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
  51. }
  52. @Override
  53. public boolean dispatchNestedPreFling( float velocityX, float velocityY) {
  54. return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
  55. }
  56. }

I won’t explain the code, after all, this is not the focus of this article. You can go online to check out NestedScrollView related articles or source code to compare and understand.

The complete layout code is quite long and the space is limited. You can check it on github: https://github.com/Moosphan/CommentWithReplyView-master

<<:  After technical people master these 11 tips, work will not be so tiring!

>>:  Is it good or bad for programmers to encounter such a leader during code review?

Recommend

Stock, increment and retention in product operation

In the process of product operation , data or tec...

Yu Jiawen did a nationwide marketing campaign without spending a penny

Yu Jiawen became famous . I have no interest in d...

Lamborghini holds new Urus SE product launch in Shanghai

[Shanghai, May 25, 2024] Following its global deb...

New discovery! The origin story of angiosperms hidden in "stones"

Produced by: Science Popularization China Author:...

What are the thoughts of the giants bidding for Toshiba's chip business?

If the global chip companies were ranked, Toshiba...

Information flow promotion: content strategy practice!

What do information flow products do? How to do i...

Frost Descent | Why does frost-bitten cabbage taste better?

=================================================...

So so so so so so so so beautiful!

October 19th, local time NASA announced The James...

World Economic Forum: 2023-2024 Fourth Industrial Revolution Network Center

The World Economic Forum has released its report ...

Did a main-belt comet collide with the source of life?

Where does the water on Earth come from? Recently...

You are just one cup of Helicobacter pylori away from the Nobel Prize

In October 2005, Australian doctors Barry Mashall...