怎么在Android项目中实现一个评论与回复功能?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。
布局中定义
首先,我们需要在xml的布局文件中声明ExpandableListView:
<ExpandableListView
android:id="@+id/detail_page_lv_comment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="@null"
android:layout_marginBottom="64dp"
android:listSelector="@android:color/transparent"
android:scrollbars="none"/>
这里需要说明两个问题:
ExpandableListView默认为它的item加上了点击效果,由于item里面还包含了childItem,所以,点击后,整个item里面的内容都会有点击效果。我们可以取消其点击特效,避免其影响用户体验,只需要设置如上代码中的listSelector即可。
ExpandableListView具有默认的分割线,可以通过divider属性将其隐藏。
设置Adapter
正如使用listView那样,我们需要为ExpandableListView设置一个适配器Adapter,为其绑定数据和视图。ExpandableListView的adapter需要继承自ExpandableListAdapter,具体代码如下:
/**
* Author: Moos
* E-mail: moosphon@gmail.com
* Date: 18/4/20.
* Desc: 评论与回复列表的适配器
*/
public class CommentExpandAdapter extends BaseExpandableListAdapter {
private static final String TAG = "CommentExpandAdapter";
private List<CommentDetailBean> commentBeanList;
private Context context;
public CommentExpandAdapter(Context context, List<CommentDetailBean> commentBeanList) {
this.context = context;
this.commentBeanList = commentBeanList;
}
@Override
public int getGroupCount() {
return commentBeanList.size();
}
@Override
public int getChildrenCount(int i) {
if(commentBeanList.get(i).getReplyList() == null){
return 0;
}else {
return commentBeanList.get(i).getReplyList().size()>0 ? commentBeanList.get(i).getReplyList().size():0;
}
}
@Override
public Object getGroup(int i) {
return commentBeanList.get(i);
}
@Override
public Object getChild(int i, int i1) {
return commentBeanList.get(i).getReplyList().get(i1);
}
@Override
public long getGroupId(int groupPosition) {
return groupPosition;
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return getCombinedChildId(groupPosition, childPosition);
}
@Override
public boolean hasStableIds() {
return true;
}
boolean isLike = false;
@Override
public View getGroupView(final int groupPosition, boolean isExpand, View convertView, ViewGroup viewGroup) {
final GroupHolder groupHolder;
if(convertView == null){
convertView = LayoutInflater.from(context).inflate(R.layout.comment_item_layout, viewGroup, false);
groupHolder = new GroupHolder(convertView);
convertView.setTag(groupHolder);
}else {
groupHolder = (GroupHolder) convertView.getTag();
}
Glide.with(context).load(R.drawable.user_other)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.error(R.mipmap.ic_launcher)
.centerCrop()
.into(groupHolder.logo);
groupHolder.tv_name.setText(commentBeanList.get(groupPosition).getNickName());
groupHolder.tv_time.setText(commentBeanList.get(groupPosition).getCreateDate());
groupHolder.tv_content.setText(commentBeanList.get(groupPosition).getContent());
groupHolder.iv_like.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(isLike){
isLike = false;
groupHolder.iv_like.setColorFilter(Color.parseColor("#aaaaaa"));
}else {
isLike = true;
groupHolder.iv_like.setColorFilter(Color.parseColor("#FF5C5C"));
}
}
});
return convertView;
}
@Override
public View getChildView(final int groupPosition, int childPosition, boolean b, View convertView, ViewGroup viewGroup) {
final ChildHolder childHolder;
if(convertView == null){
convertView = LayoutInflater.from(context).inflate(R.layout.comment_reply_item_layout,viewGroup, false);
childHolder = new ChildHolder(convertView);
convertView.setTag(childHolder);
}
else {
childHolder = (ChildHolder) convertView.getTag();
}
String replyUser = commentBeanList.get(groupPosition).getReplyList().get(childPosition).getNickName();
if(!TextUtils.isEmpty(replyUser)){
childHolder.tv_name.setText(replyUser + ":");
}
childHolder.tv_content.setText(commentBeanList.get(groupPosition).getReplyList().get(childPosition).getContent());
return convertView;
}
@Override
public boolean isChildSelectable(int i, int i1) {
return true;
}
private class GroupHolder{
private CircleImageView logo;
private TextView tv_name, tv_content, tv_time;
private ImageView iv_like;
public GroupHolder(View view) {
logo = view.findViewById(R.id.comment_item_logo);
tv_content = view.findViewById(R.id.comment_item_content);
tv_name = view.findViewById(R.id.comment_item_userName);
tv_time = view.findViewById(R.id.comment_item_time);
iv_like = view.findViewById(R.id.comment_item_like);
}
}
private class ChildHolder{
private TextView tv_name, tv_content;
public ChildHolder(View view) {
tv_name = (TextView) view.findViewById(R.id.reply_item_user);
tv_content = (TextView) view.findViewById(R.id.reply_item_content);
}
}
}
一般情况下,我们自定义自己的ExpandableListAdapter后,需要实现以下几个方法:
构造方法,这个应该无需多说了,一般用来初始化数据等操作。
getGroupCount,返回group分组的数量,在当前需求中指代评论的数量。
getChildrenCount,返回所在group中child的数量,这里指代当前评论对应的回复数目。
getGroup,返回group的实际数据,这里指的是当前评论数据。
getChild,返回group中某个child的实际数据,这里指的是当前评论的某个回复数据。
getGroupId,返回分组的id,一般将当前group的位置传给它。
getChildId,返回分组中某个child的id,一般也将child当前位置传给它,不过为了避免重复,可以使用getCombinedChildId(groupPosition, childPosition);来获取id并返回。
hasStableIds,表示分组和子选项是否持有稳定的id,这里返回true即可。
isChildSelectable,表示分组中的child是否可以选中,这里返回true。
getGroupView,即返回group的视图,一般在这里进行一些数据和视图绑定的工作,一般为了复用和高效,可以自定义ViewHolder,用法与listview一样,这里就不多说了。
getChildView,返回分组中child子项的视图,比较容易理解,第一个参数是当前group所在的位置,第二个参数是当前child所在位置。
这里的数据是我自己做的模拟数据,不过应该算是较为通用的格式了,大体格式如下:
一般情况下,我们后台会通过接口返回给我们一部分数据,如果想要查看更多评论,需要跳转到“更多页面”去查看,这里为了方便,我们只考虑加载部分数据。
Activity中使用
接下来,我们就需要在activity中显示评论和回复的二级列表了:
private ExpandableListView expandableListView;
private CommentExpandAdapter adapter;
private CommentBean commentBean;
private List<CommentDetailBean> commentsList;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
expandableListView = findViewById(R.id.detail_page_lv_comment);
initExpandableListView(commentsList);
}
/**
* 初始化评论和回复列表
*/
private void initExpandableListView(final List<CommentDetailBean> commentList){
expandableListView.setGroupIndicator(null);
//默认展开所有回复
adapter = new CommentExpandAdapter(this, commentList);
expandableListView.setAdapter(adapter);
for(int i = 0; i<commentList.size(); i++){
expandableListView.expandGroup(i);
}
expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
@Override
public boolean onGroupClick(ExpandableListView expandableListView, View view, int groupPosition, long l) {
boolean isExpanded = expandableListView.isGroupExpanded(groupPosition);
Log.e(TAG, "onGroupClick: 当前的评论id>>>"+commentList.get(groupPosition).getId());
// if(isExpanded){
// expandableListView.collapseGroup(groupPosition);
// }else {
// expandableListView.expandGroup(groupPosition, true);
// }
return true;
}
});
expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView expandableListView, View view, int groupPosition, int childPosition, long l) {
Toast.makeText(MainActivity.this,"点击了回复",Toast.LENGTH_SHORT).show();
return false;
}
});
expandableListView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
@Override
public void onGroupExpand(int groupPosition) {
//toast("展开第"+groupPosition+"个分组");
}
});
}
/**
* by moos on 2018/04/20
* func:生成测试数据
* @return 评论数据
*/
private List<CommentDetailBean> generateTestData(){
Gson gson = new Gson();
commentBean = gson.fromJson(testJson, CommentBean.class);
List<CommentDetailBean> commentList = commentBean.getData().getList();
return commentList;
}
就以上代码作一下简单说明:
1、ExpandableListView在默认情况下会为我们自带分组的icon(▶️),当前需求下,我们根本不需要展示,可以通过expandableListView.setGroupIndicator(null)来隐藏。
2、一般情况下,我们可能需要默认展开所有的分组,我就可以通过循环来调用expandableListView.expandGroup(i);方法。
3、ExpandableListView为我们提供了group和child的点击事件,分别通过setOnGroupClickListener和setOnChildClickListener来设置。值得注意的是,group的点击事件里如果我们返回的是false,那么我们点击group就会自动展开,但我这里碰到一个问题,当我返回false时,第一条评论数据会多出一条。通过百度查找方法,虽然很多类似问题,但终究没有解决,最后我返回了ture,并通过以下代码手动展开和收缩就可以了:
if(isExpanded){
expandableListView.collapseGroup(groupPosition);
}else {
expandableListView.expandGroup(groupPosition, true);
}
4、此外,我们还可以通过setOnGroupExpandListener和setOnGroupCollapseListener来监听ExpandableListView的分组展开和收缩的状态。
评论和回复功能
为了模拟整个评论和回复功能,我们还需要手动插入收据并刷新数据列表。这里我就简单做一下模拟,请忽略一些UI上的细节。
插入评论数据
插入评论数据比较简单,只需要在list中插入一条数据并刷新即可:
String commentContent = commentText.getText().toString().trim();
if(!TextUtils.isEmpty(commentContent)){
//commentOnWork(commentContent);
dialog.dismiss();
CommentDetailBean detailBean = new CommentDetailBean("小明", commentContent,"刚刚");
adapter.addTheCommentData(detailBean);
Toast.makeText(MainActivity.this,"评论成功",Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(MainActivity.this,"评论内容不能为空",Toast.LENGTH_SHORT).show();
}
adapter中的addTheCommentData方法如下:
/**
* by moos on 2018/04/20
* func:评论成功后插入一条数据
* @param commentDetailBean 新的评论数据
*/
public void addTheCommentData(CommentDetailBean commentDetailBean){
if(commentDetailBean!=null){
commentBeanList.add(commentDetailBean);
notifyDataSetChanged();
}else {
throw new IllegalArgumentException("评论数据为空!");
}
}
代码比较容易理解,就不多做说明了。
插入回复数据
首先,我们需要实现点击某一条评论,然后@ta,那么我们需要在group的点击事件里弹起回复框:
expandableListView.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() {
@Override
public boolean onGroupClick(ExpandableListView expandableListView, View view, int groupPosition, long l) {
showReplyDialog(groupPosition);
return true;
}
});
......
/**
* by moos on 2018/04/20
* func:弹出回复框
*/
private void showReplyDialog(final int position){
dialog = new BottomSheetDialog(this);
View commentView = LayoutInflater.from(this).inflate(R.layout.comment_dialog_layout,null);
final EditText commentText = (EditText) commentView.findViewById(R.id.dialog_comment_et);
final Button bt_comment = (Button) commentView.findViewById(R.id.dialog_comment_bt);
commentText.setHint("回复 " + commentsList.get(position).getNickName() + " 的评论:");
dialog.setContentView(commentView);
bt_comment.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String replyContent = commentText.getText().toString().trim();
if(!TextUtils.isEmpty(replyContent)){
dialog.dismiss();
ReplyDetailBean detailBean = new ReplyDetailBean("小红",replyContent);
adapter.addTheReplyData(detailBean, position);
Toast.makeText(MainActivity.this,"回复成功",Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(MainActivity.this,"回复内容不能为空",Toast.LENGTH_SHORT).show();
}
}
});
commentText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
if(!TextUtils.isEmpty(charSequence) && charSequence.length()>2){
bt_comment.setBackgroundColor(Color.parseColor("#FFB568"));
}else {
bt_comment.setBackgroundColor(Color.parseColor("#D8D8D8"));
}
}
@Override
public void afterTextChanged(Editable editable) {
}
});
dialog.show();
}
插入回复的数据与上面插入评论类似,这里贴一下adapter中的代码:
/**
* by moos on 2018/04/20
* func:回复成功后插入一条数据
* @param replyDetailBean 新的回复数据
*/
public void addTheReplyData(ReplyDetailBean replyDetailBean, int groupPosition){
if(replyDetailBean!=null){
Log.e(TAG, "addTheReplyData: >>>>该刷新回复列表了:"+replyDetailBean.toString() );
if(commentBeanList.get(groupPosition).getReplyList() != null ){
commentBeanList.get(groupPosition).getReplyList().add(replyDetailBean);
}else {
List<ReplyDetailBean> replyList = new ArrayList<>();
replyList.add(replyDetailBean);
commentBeanList.get(groupPosition).setReplyList(replyList);
}
notifyDataSetChanged();
}else {
throw new IllegalArgumentException("回复数据为空!");
}
}
需要注意一点,由于不一定所有的评论都有回复数据,所以在插入数据前我们要判断ReplyList是否为空,如果不为空,直接获取当前评论的回复列表,并插入数据;如果为空,需要new一个ReplyList,插入数据后还要为评论set一下ReplyList。
解决CoordinatorLayout与ExpandableListView嵌套问题
如果你不需要使用CoordinatorLayout或者NestedScrollView,可以跳过本小节。一般情况下,我们产品为了更好的用户体验,还需要我们加上类似的顶部视差效果或者下拉刷新等,这就要我们处理一些常见的嵌套滑动问题了。
由于CoordinatorLayout实现NestedScrollingParent接口,RecycleView实现了NestedScrollingChild接口,所以就可以在NestedScrollingChildHelper的帮助下实现嵌套滑动,那么我们也可以通过自定义的ExpandableListView实现NestedScrollingChild接口来达到同样的效果:
/**
* Author: Moos
* E-mail: moosphon@gmail.com
* Date: 18/4/20.
* Desc: 自定义ExpandableListView,解决与CoordinatorLayout滑动冲突问题
*/
public class CommentExpandableListView extends ExpandableListView implements NestedScrollingChild{
private NestedScrollingChildHelper mScrollingChildHelper;
public CommentExpandableListView(Context context, AttributeSet attrs) {
super(context, attrs);
mScrollingChildHelper = new NestedScrollingChildHelper(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setNestedScrollingEnabled(true);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mScrollingChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mScrollingChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mScrollingChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mScrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
}
关于怎么在Android项目中实现一个评论与回复功能问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注亿速云行业资讯频道了解更多相关知识。
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。