前言

各位应该对黄易新闻比较熟悉,其中评论区一般都会出现一些盖楼的神评论,今天的主题就是仿照做一个有盖楼效果的评论列表。

首先上图给大家看下效果:

思路

数据结构设计

首先分析看下评论的结构,仔细观摩下发现,有的评论是简单一条,只有用户头像,昵称,评论内容等;有的评论是回复别人的评论,这样就不只是有用户头像,评论内容等基础信息,还有回复的别人的内容,这就是所谓“盖楼”了。那么怎么设计评论的model可以包含评论的所有数据呢?

首先满足简单评论的model很容易,如下:

public class Comment {private int id;private String mReplyTime;private String mAvaterUrl;private String mUsername;private String mUserArea;private String mCommentContent;private int mFavorCount;//getter setter...}

以上包括了一条简单评论需要展示项的所有信息,可是如果是回复其他评论的评论,这种存在“盖楼”的评论项,这种结构就无法满足了。吃根辣条冷静一下,再分析盖楼中的内容,发现盖楼中的一项内容其实是被回复的评论,其中包括了用户昵称,评论内容。
总结来说,一条评论中可能包括了另一条评论,那么在Comment中添加一个字段:

private Comment replyTo;

这个字段保存了其回复的评论,这样就能形成一个类似单向链表的结构了,一条评论项中能够与它回复的评论关联起来,并且能够依次链接,那么所有回复的评论内容都能在链表中找到,结构清晰明了,那么model就已经设计好了。

界面设计

很明显这是一个列表结构,最外层咱们可以使用Recyclerview来实现,每个评论项可以使用一个自定义View封装起来,这个自定义View来处理数据和UI的适配。我们来分析下这个View的构成:
1. 用户头像,昵称,回复内容等都是基础内容,每个评论项都会展示出这些信息。
2. 有的回复其他评论的评论项中间会多出一个评论“楼层”。

这样简单一分析,我们就有了思路了,这个自定义的View可以分解成两个部分,一个部分就是基础信息展示,另个就是评论“楼层”展示。布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"><RelativeLayout
        android:layout_width="match_parent"android:layout_height="wrap_content"><io.geek.myapplication.view.CircleView2
            android:id="@+id/iv_avater"android:layout_width="50dp"android:layout_height="50dp"android:layout_alignParentLeft="true"android:layout_alignParentStart="true"android:layout_alignParentTop="true"/><TextView
            android:id="@+id/tv_username"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="5dp"android:layout_toRightOf="@+id/iv_avater"android:text="Geek"/><TextView
            android:id="@+id/tv_user_area"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_below="@+id/tv_username"android:layout_toEndOf="@+id/iv_avater"android:layout_toRightOf="@+id/iv_avater"android:text="来自火星的网友"/><TextView
            android:id="@+id/tv_reply_time"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignBottom="@+id/tv_user_area"android:layout_marginLeft="5dp"android:layout_marginRight="5dp"android:layout_marginTop="5dp"android:layout_toRightOf="@+id/tv_user_area"android:text="33分钟前"/><TextView
            android:id="@+id/favor_count"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentRight="true"android:text="2896"android:textSize="8dp"/></RelativeLayout><io.geek.myapplication.comment.CommentFloorView
        android:id="@+id/comment_floor"android:layout_width="match_parent"android:layout_height="wrap_content"android:visibility="gone"/><TextView
        android:id="@+id/tv_comment_content"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="我只是一个普通的评论。"/>
</merge>

tips: 使用merge标签能够减少布局层级,减少界面渲染次数,优化性能。

以上是我们自定义CommentItemView的布局文件,现在我们来编写CommentItemView,这个自定义View继承LinearLayout,然后构造方法中初始化各个需要绑定数据的view,暴露一个绑定数据的bind方法,这样就能把数据展示在对应的view上了。代码如下:

public class CommentItemView extends LinearLayout {CircleImageView avater;TextView tvUsername;TextView tvUserArea;TextView tvReplyTime;TextView tvFavorCount;CommentFloorView commentFloorView;TextView tvCommentContent;Comment mComment;public CommentItemView(Context context) {this(context, null);}public CommentItemView(Context context, @Nullable AttributeSet attrs) {this(context, attrs, 0);}public CommentItemView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {this(context, attrs, defStyleAttr, 0);}public CommentItemView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr);initView(context);}private void initView(Context context) {LayoutInflater.from(context).inflate(R.layout.list_item_view_comment, this,true);int padding = getResources().getDimensionPixelOffset(R.dimen.activity_horizontal_margin);setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));setOrientation(VERTICAL);setPadding(padding, padding, padding, padding);avater = (CircleImageView) findViewById(R.id.iv_avater);tvUsername = (TextView) findViewById(R.id.tv_username);tvUserArea = (TextView) findViewById(R.id.tv_user_area);tvReplyTime = (TextView) findViewById(R.id.tv_reply_time);commentFloorView = (CommentFloorView) findViewById(R.id.comment_floor);tvCommentContent = (TextView) findViewById(R.id.tv_comment_content);}public void bind(Comment comment) {mComment = comment;tvUsername.setText(comment.getUsername());tvUserArea.setText(comment.getUserArea());tvReplyTime.setText(comment.getReplyTime());tvCommentContent.setText(comment.getCommentContent());commentFloorView = (CommentFloorView) findViewById(R.id.comment_floor);setupCommentFloorView();}private void setupCommentFloorView() {List<Comment> replyFloor = getReplyFloor();if (replyFloor != null) {commentFloorView.setVisibility(VISIBLE);commentFloorView.updateData(replyFloor);} else {commentFloorView.setVisibility(GONE);}}//通过comment中的replyTo字段找到其回复的评论链,按照回复的顺序排列放入list中private List<Comment> getReplyFloor( ) {List<Comment> floorData = new ArrayList<>();if (mComment == null || mComment.getReplyTo() == null) {return null;}Comment reply = mComment.getReplyTo();while (reply != null) {floorData.add(reply);reply = reply.getReplyTo();}//按照回复的顺序来排列Collections.reverse(floorData);return floorData;}
}

上面的代码结构很简单,没有啥复杂的逻辑,就是一个简单点的封装了各个控件的容器,然后有一个绑定数据的方法。可能你看到一个陌生的view:CommentFloorView,以下就是本文需要讲的重点!!和难点!!

CommentFloorView也是我们一个自定义view,这个view负责的是评论“楼层”展示的,主体构成是Recylerview,我们来仔细分析下这个评论“楼层”,本质就是一个列表,列表中展示的是评论用户名和评论的内容,如果这个列表超过了一定的数量就会折叠展示,点击展开楼层会全部展示,并且每个项会有边框,并不是简单的每个列表项套一个边框,而是看起来会有层叠效果的边框。通过这样一分析,总结出实现这个view的两个难点:
1. 展开和隐藏楼层。
2. 边框的绘制。

  • 难点一

这个recylerview中展示的有两种view,其一就是评论项,其二就是展开/隐藏楼层的按钮。熟悉recylerview功能应该会知道重写adapter中的getItemType方法可以实现这种一个recylerview展示不同的类型item。

首先解析展开和隐藏楼层功能,重写getItemCount方法,代码如下,解析在注释:

        @Overridepublic int getItemCount() {if (hasHideFloor()) {if (isFloorExpanded) {//如果有有隐藏楼层,并且楼层是展开的状态//加的1是因为最后还有一个隐藏楼层的项return mComments.size() + 1;} else {//如果有隐藏楼层,并且没有展开,那么只有4项,分别是第一二项数据,展开楼层按钮和最后一项数据return 4;}} else {//如果没有隐藏楼层,则返回数据的数量return mComments.size();}}//如果数据量大于一个默认值(这里设定的是5个),那么默认会有隐藏楼层,小于的话就无需隐藏private boolean hasHideFloor() {return mComments.size() > EXPAND_LIMIT_COMMENT_COUNT;}

然后重现getItemType方法,只要把逻辑理清楚了其实也很简单,代码如下:

        @Overridepublic int getItemViewType(int position) {if (hasHideFloor()) {if (isFloorExpanded) {if (position == mComments.size()) {//如果有隐藏楼层,并且楼层是展开状态,并且是最后一项,那么返回展开/收起按钮布局IDreturn R.layout.list_item_hide_expand_floor;} else {//如果有隐藏楼层,并且楼层是展开状态,不是最后一项,返回评论项布局IDreturn R.layout.list_item_simple_reply_comment;}} else {if (position == EXPAND_FLOOR_ITEM_POSITION) {//如果有隐藏楼层,并且楼层是隐藏状态,并且是指定的展开楼层按钮位置,那么返回展开/收起按钮布局IDreturn R.layout.list_item_hide_expand_floor;} else {//如果有隐藏楼层,并且楼层是隐藏状态,不是展开楼层按钮位置,那么评论项布局IDreturn R.layout.list_item_simple_reply_comment;}}} else {//如果没有隐藏楼层,那么都是返回评论布局IDreturn R.layout.list_item_simple_reply_comment;}}

重写好了以上两个方法,就能配合onCreateViewHolder和onBindViewHolder方法来实现列表中不同位置生成不同的View,并且绑定响应的数据项。

这样就能实现UI效果了,但是有个棘手的问题来了,怎么做到点击展开/隐藏楼层后真的能展开和隐藏部分item呢?不要慌,吃根辣条冷静分析下,我们已经重写了getItemCount和getItemType方法,判断逻辑中有利用判定楼层展开和隐藏的字段isFloorExpaned来返回不同的item个数和控制在不同位置返回不同的view,其实实现展开和隐藏楼层的核心部分就写完了,现在我们只需要控制这个isFloorExpanded的值就行了,具体实现就是编写一个监听点击展开/隐藏楼层按钮的接口,然后在adapter构造方法中创建一个监听器,传入到展开/隐藏楼层按钮的ViewHolder中,在点击这个view的时候触发监听器,修改isFloorExpanded的值,并且notifyDataSetChanged()方法,重新刷新recylerview。核心代码如下:

 //这个adapter是编写在CommentFloorView中的,所以使用了static来修饰public static class CommentFloorAdapter extends RecyclerView.Adapter {private List<Comment> mComments = new ArrayList<>();private OnFloorExpandListener mOnFloorExpandListener;private boolean isFloorExpanded;public CommentFloorAdapter() {mOnFloorExpandListener = new OnFloorExpandListener() {@Overridepublic void onFloorExpand(boolean isExpand) {isFloorExpanded = isExpand;notifyDataSetChanged();}};}//.....public interface OnFloorExpandListener {void onFloorExpand(boolean isExpand);}static class HideExpandFloorVH extends RecyclerView.ViewHolder {FrameLayout mContentView;TextView tips;OnFloorExpandListener mOnFloorExpandListener;public HideExpandFloorVH(View itemView, OnFloorExpandListener onFloorExpandListener, final boolean isFloorExpanded) {super(itemView);mOnFloorExpandListener = onFloorExpandListener;mContentView = (FrameLayout) itemView;tips = (TextView) mContentView.findViewById(R.id.tv_is_show_all);mContentView.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {mOnFloorExpandListener.onFloorExpand(!isFloorExpanded);}});}public void changeTips(boolean isFloorExpanded) {tips.setText(isFloorExpanded ? "收起展开楼层" : "展开隐藏楼层");}}}

tips:内部类最好使用static修饰,这样内部类不会持有外部类的强应用,防止内存泄露,优化性能。

这样核心的功能就已经实现啦~此时的实现效果如下图:


这样看着和原版的还是有点差距的,这是没有绘制边框的原因,吃根辣条休息下,接下来开始解析难点二了。

  • 难点二

还是先要分析下这个边框是个啥玩意,看着是有层叠的效果。细细看了下,发现它的规律是第一个绘制一个边框,然后第一个和第二个作为整体外层再绘制一个边框,再前三个作为一个整体绘制边框,依次类推。这样就产生了一种层叠的效果。绘制的规律被我们发现了,那具体如何绘制呢?recyclerview中有一个方法是addItemDecoration,利用ItemDecoration我们可以给每个item绘制边框或者分割线,这里我们定义一个CommentFloorItemDecoration类,继承自ItemDecoration,重写其中的onDraw和getItemOffsets方法,代码如下:

public class CommentFloorItemDecoration extends RecyclerView.ItemDecoration {private static float BORDER_OFFSET;private static float BORDER_WIDTH;private Context mContext;private Paint mBorderPaint;public CommentFloorItemDecoration(Context context) {mContext = context;BORDER_OFFSET = context.getResources().getDisplayMetrics().density * 1;BORDER_WIDTH = context.getResources().getDisplayMetrics().density * 1;mBorderPaint = new Paint();mBorderPaint.setAntiAlias(true);mBorderPaint.setColor(Color.LTGRAY);mBorderPaint.setStyle(Paint.Style.STROKE);mBorderPaint.setStrokeWidth(BORDER_WIDTH);}@Overridepublic void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {int itemCount = parent.getChildCount();int top = parent.getChildAt(0).getTop();for (int i = 0; i < itemCount; i++) {View child = parent.getChildAt(i);int left = child.getLeft();int right = child.getRight();int bottom = child.getBottom();c.drawRect(left, top, right, bottom, mBorderPaint);top -= (BORDER_WIDTH + BORDER_OFFSET);}}@Overridepublic void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {super.getItemOffsets(outRect, view, parent, state);RecyclerView.Adapter adapter = parent.getAdapter();int itemCount = adapter.getItemCount();int i = parent.getChildAdapterPosition(view);int top = (int) ((itemCount - i) * BORDER_WIDTH + (itemCount - i - 1) * BORDER_OFFSET);int left = top;int right = top;int bottom = (int) BORDER_WIDTH;if (i == 0) {outRect.set(left, top, right, bottom);} else {outRect.set(left, 0, right, bottom);}}
}

getItemOffset的作用是得到每个item的偏移量,重写此方法可以给每个item设置偏移量,在这里我们按照上面总结出的规律给不同的item设置的不同的offset值,然后再重写onDraw方法,给item绘制边框,具体逻辑参看代码。最后我们就完整实现盖楼效果了。

全文就此完毕~( 注意: 此文主要是提供一个思路,和原版细节方面有一定出入,并且没有运用到实际项目中过,需要使用的同学请自行测试)

仿网易新闻评论“盖楼”效果实现相关推荐

  1. Android网易新闻评论盖楼效果的实现

    转子:http://xie2010.blog.163.com/blog/static/211317365201411041930806/

  2. 仿网易新闻评论的楼层效果

    评论在很多app中都会用到,最常见的就是网易新闻中的评论了,所以今天就研究了一下这个,先看看网易的效果图吧. 这么一个评论的列表,想必都会做吧.一个ListView之类的控件,里面的item也是一个较 ...

  3. 【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(二)——大图显示模式、评论“盖楼”效果实现详解

    转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992 在前一篇文章中,我们学习了如何进行逆向工程和TcpDump进行抓包,获取我们的数据接口,那么有了数据之后,我 ...

  4. 仿网易新闻顶部菜单html,iOS仿网易新闻滚动导航条效果

    本文实例为大家分享了iOS滚动导航条效果展示的具体代码,供大家参考,具体内容如下 实现效果 效果:选择不同的栏目,下面出现不同的视图,栏目条可以滚动:下面的视图也可以滚动,滚动时上面对应的栏目要选中颜 ...

  5. Android网易评论盖楼效果实现

    下面是一个主要的方法: /** * 递归加载楼层的方法 * * @param context上下文的对像 * @param 递归的控制参数 * ,同时也是取用户评论信息和背景色的下标,引参数的大小必须 ...

  6. android 评论盖楼,Android 使用ListView实现网易评论盖楼效果

    效果如下:( 点击下载演示) 本人已经开源到了TaoCode,可以使用SVN免费更新下来:http://code.taobao.org/svn/nestlistview/trunk 实现原理:顶部利用 ...

  7. mysql实现评论盖楼的sql_SQL递归查询实现跟帖盖楼效果

    网易新闻的盖楼乐趣多,某一天也想实现诸如网易新闻跟帖盖楼的功能,无奈技术不佳(基础不牢),网上搜索了资料才发现SQL查询方法有一种叫递归查询,整理如下: 一.查询出 id = 1 的所有子结点 wit ...

  8. 网易评论盖楼php,DedeCMS评论引用美化:仿腾讯/网易盖楼效果

    关于DedeCMS的使用教程,烈火介绍了很多,相信各位站长都有所了解,今天我们来看一个美化评论样式,实现仿腾讯.网易.迅雷等的盖楼效果,这是笔者前段时间就美化了的,当初烈火只是仿了腾讯的样式,在盖楼时 ...

  9. php实现和MySQL实现评论_仿网易评论盖楼PHP+Mysql实现

    这篇文章主要介绍了关于仿网易评论盖楼PHP+Mysql实现,有着一定的参考价值,现在分享给大家,有需要的朋友可以参考一下 大家可能都看过网易评论的那种盖楼式的引用,这边文章就用php和mysql来实现 ...

最新文章

  1. Mail group(转至毅冰)
  2. SAP 系统参数设置 RZ10 RZ11
  3. flask中的CBV和FBV
  4. JavaScript设计模式与开发实践 | 02 - this、call和apply
  5. 根据HTML5 获取当前位置的经纬度【百度地图】【高德地图】
  6. 怎样Interlocked.Increment一个反射得到的field?
  7. ionic 组件之二维码扫描
  8. 花书+吴恩达深度学习(九)优化方法之二阶近似方法(牛顿法, CG, BFGS, L-BFGS)
  9. 分布式存储绝不简单 —— UCan下午茶-武汉站纪实
  10. php sqlsrv 分页,sqlsrv php分页
  11. Oracle sql 错误 : ORA-01861: 文字与格式字符串不匹配和日期与字符串互转问题解决
  12. python使用函数输出指定范围内fibonacci数的个数_第6章函数-4 使用函数输出指定范围内Fibonacci数的个数...
  13. 一场暴雨引发的装机日记
  14. 一些NLP数据/语料下载
  15. IOS个人开发者账号和wp公司开发者帐号申请注意点
  16. idea用JAVA连接mysqlAccess denied for user ‘root‘@‘localhost‘ (using password: YES)错误
  17. 喾哲~ (八月最佳)
  18. android animation
  19. 面试必问题之Docker分布式搭建
  20. LeetCode376 摇摆序列

热门文章

  1. lcg_magic算法笔记:堆排序
  2. [kubernetes]-挂载nfs出错排查
  3. php拆分jsion_json返回的文本 分割问题!
  4. sqlserver数据库清理(收缩文件)
  5. 用python画小黄人
  6. 用 .NET / C# 实现录屏小程序并保存为视频文件
  7. 图论-全源最短路径-对比Floyd算法与暴力Dijkstra算法
  8. 如何在discuz帖子中插入视频
  9. 七星彩2007年开奖结果_7星彩历年开奖号码(2004年至2020年11月)
  10. Magic cloth使用方法