【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(二)——大图显示模式、评论“盖楼”效果实现详解
来源:互联网 发布:日本心神战斗机 知乎 编辑:程序博客网 时间:2024/04/30 12:12
转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992
在前一篇文章中,我们学习了如何进行逆向工程和TcpDump进行抓包,获取我们的数据接口,那么有了数据之后,我们就可以开始代码编写工作了。
本项目在前几天获得了daimajia大神的推荐,star数已经达到115,多谢大家的支持,欢迎提建议和意见。
项目地址:https://github.com/ZhaoKaiQiang/JianDan
- 项目进度
- 已完成的功能
- 优化的功能
- 想完成但没有完成的功能
- 效果图
- 项目整体架构介绍
- 使用的开源框架
- 项目整体介绍
- 项目中遇到的问题及解决方案
- 加载任意高度的图片
- 评论的楼中楼多楼隐藏效果实现
目前项目已完成以下功能,本文章将会总结在编码过程中遇到的挑战和解决方案。
项目进度
已完成的功能
- 查看段子
- 查看无聊图(静态图和GIF动态图)
- 查看妹子图(程序员必备)
- 对段子、无聊图、妹子图进行投票
- 段子的复制与分享
- 无聊图、妹子图的保存与分享
- 查看吐槽与回复
- 图片详情页的动画效果
- 添加新鲜事列表页
- 添加新鲜事详情页
优化的功能
- 添加加载等待动画
- 添加加载失败提示
- 添加段子列表界面,点击标题栏快速返回顶端
- 添加评论楼层过多隐藏
- 添加网络状态检测
- 优化无聊图列表显示,非WIFI状态下,显示GIF缩略图,点击后下载
- 加载模式全自动智能切换,显著提高加载速度,节省大量流量
- 修改图片详情页为完全沉浸效果
- 图片详情页添加投票结果的颜色标示
- 添加图片列表滚动检测,滚动状态暂停加载,进一步提高加载速度,减少卡顿
- 添加图片加载图片
- 添加当前栏目标志,避免重复切换
- 修改新鲜事列表页效果为CardView
想完成但没有完成的功能
- 列表加载动画(虽然试过好多次,但是都不能实现首次加载,CardView进入时的动画效果,如果你能知道我如何实现,我将非常感激)
- 本地缓存(后期将添加)
效果图
项目整体架构介绍
使用的开源框架
- Volley
- Fresco
- Universal Image Load
- butter knife
- EventBus
- material-dialogs
- gson
项目整体介绍
从上面的效果图也可以看出来,我们使用的是Material Design风格,但是并不纯正,为了兼容4.x版本,我们使用Theme.AppCampat兼容主题、RecycleView和CardView来完成,从整体视觉效果来看比较统一和美观。同时为了整体的效果,使用开源项目material-dialogs来实现Material Design效果的对话框,这个在点击回复,完善个人信息的功能点上有所体现。
除了界面,网络请求框架我选择的是Volley,原因是Volley对小数据量、请求频繁的网络操作进行了优化,对于这个项目比较合适,而且作为Google的推荐项目,现在已经完善的比较成熟了,经过了很多项目的实战验证,所以比较放心。而且扩展性非常强,可以定制我们自己的请求解析需求,这一点相信看过我项目的朋友,应该有所感受,在com.socks.jiandan.net包下的请求类都经过了我的定制,使用方便。而且很重要的一点是,Volley在2.3之后是基于HttpURLConnection的封装实现,默认支持gzip压缩,在4.0之后的版本,还支持结果缓存,所以在性能和数据传输量上,相比HttpClient有很大的提高。
在本项目中一个很重要的功能就是加载图片,所以在图片加载框架上需要特别注意。最初我选择的图片加载框架是Fresco,因为之前翻译过关于Fresco的特性的文章,感觉非常的强大,所以想试一试。但是在后面使用的时候,还是遇到了很多的问题,让我不得不暂时放弃Fresco,改用UIL。原因如下:
- 推出时间太短,虽然功能强大,但是还没经过考验,还不很成熟。Fresco的更新频率很快,我开始用的时候还是0.1.0版本,后来在加载图片的时候遇到问题,在这个版本上,Fresco没有对有304缓存的图片进行处理,所以在加载这类图片的时候会出现失败,我给Fresco项目提交issue之后,他们回复我,Fresco已经升级,在0.2.0完成了问题修复。所以我觉得,Fresco还需要一段时间的考验和完善,才能被用到生产环境中,现在我不很推荐大家在项目中使用
- 不支持wrap_content。放弃Fresco的一个很重要的原因就是因为它不支持wrap_content,Fresco只支持match和固定长宽,在这个项目中需要展示大量宽度match,高度不定的图片,因为Fresco显示图片的控件也是自己定制的,所以自定义控件这条路也比较难走,在没有找到更好的解决方案的情况下,我决定暂时放弃Fresco,改用UIL。在本项目中,只有在评论列表页的头像是使用的Fresco,其他地方都是使用UIL和自定义控件实现,具体实现方案我会在下面讲到。
在IOC框架的选择上,使用butter knife,之前一直使用AFinal,但是AFinal属于运行期绑定,会影响性能,butter knife属于编译期绑定,不会影响。使用butter knife使用非常方便,就拿来一用。在本项目中,我感觉其实并不是很需要IOC,仅作一个尝试而已,不必深究。
在完成网络状态切换的功能上,需要在MainActivity注册一个网络状态监听器,当网络状态发生改变的时候,通知当前显示的Fragment切换图片的加载模式,或者是提示网络状态变化情况。在这种需求下,使用接口是可以完成的,每个Fragment都实现MainActivity的一个接口,当网络状态发生变化的时候,MainActivity调用Fragment的接口方法即可。但是这样不仅很麻烦,而且会增加耦合性,为此,我使用EventBus完成了这个功能,实现很简单,大家看源码就可以,耦合度为0。
这个项目中的所有数据接口基本都是Json格式,所以选择一个好的解析框架是很重要的。我之前写过三篇文章介绍了Json的不同解析方法,虽然Jackson的解析速度快,但是gson确实用起来很熟悉,而且我们要解析的数据量并不大,性能上的差异微乎其微,所以我选择了我比较熟悉的gson。在解析的一些地方还用到了一些JSON,这个大家可以自由选择。
- Json
- Gson
- Jackson
项目中遇到的问题及解决方案
加载任意高度的图片
我们在前面介绍Fresco的时候提到过,之所以放弃它,很大的一个原因就是因为这个功能它不支持,我们先来看看我们要实现功能的详细分析。
- 图片宽度要和ImageView的相同
- 在上一个条件满足的情况下,完整的显示这个图片,高度自适应
也就是下面的效果
我的解决思路是这样的,宽度和ImageView相同,那么设置为match_parent即可,高度则是wrap_content,但是这样显示之后,图片可以完整显示,但是不能符合我们宽度填充,高度自适应的要求。那么我们可能就要设置ScaleType了,但是在试过了所有类型之后,也都满足不了我们的要求,要不就是只能显示一部分,要不就是宽度不能填充,或者是不能居中显示。为此,我们可以试一试自定义控件。
我们可以设置ScaleType为centerCrop,还记得centerCrop是什么意思么?以图片几何中心为基准,放缩短边至填充满。这样做的话,第一个填充效果就可以实现了,剩下的就是要高度自适应了。
我第一次在做这个功能的时候,走入了一个误区。
第一个思路就是,重写ImageView的setBitmap和setDrawable方法,在设置之后,获取bitmap,然后计算ImageView的宽度和bitmap的比例,以此比例计算bitmap的高度,然后生成新的Bitmap对象,设置给ImageView,设置之后,调用requestLayout(),重新布局,完成高度的改变。首先,使用这个方案是完全能解决问题的,计算完之后,重新布局,可以使得高度自适应,但是,你发现问题了吗?我在计算高度之后,又重新生成了Bitmap对象,而这一步是使用下面的方法完成的
Matrix matrix = new Matrix(); matrix.postScale(1.5f,1.5f); //长和宽放大缩小的比例 Bitmap resizeBmp = Bitmap.createBitmap(bitmap,0,0,Width,height,matrix,true);
在这个操作里面,使用到了矩阵,而矩阵计算会占用大量cpu时间,因此,当我这么完成之后,慢慢滑动列表是没有问题的,但是当我疯狂的快速滑动的时候,就会出现非常明显的卡顿。
那么怎么解决这个问题呢?其实我后来看代码,完全没必要再生成新的Bitmap,只计算合适的高度就可以完成我们的需求,因此,修改之后的代码如下
/** * 自定义控件,用于显示宽度和ImageView相同,高度自适应的图片显示模式. * 除此之外,还添加了最大高度限制,若图片长度大于等于屏幕长度,则高度显示为屏幕的1/3 * Created by zhaokaiqiang on 15/4/20. */public class ShowMaxImageView extends ImageView { private float mHeight = 0; public ShowMaxImageView(Context context) { super(context); } public ShowMaxImageView(Context context, AttributeSet attrs) { super(context, attrs); } public ShowMaxImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public void setImageBitmap(Bitmap bm) { if (bm != null) { getHeight(bm); } super.setImageBitmap(bm); requestLayout(); invalidate(); } @Override public void setImageDrawable(Drawable drawable) { if (drawable != null) { getHeight(drawableToBitamp(drawable)); } super.setImageDrawable(drawable); requestLayout(); invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mHeight != 0) { int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); int resultHeight = (int) Math.max(mHeight, sizeHeight); if (resultHeight >= ScreenSizeUtil.getScreenHeight((Activity) getContext())) { resultHeight = ScreenSizeUtil.getScreenHeight((Activity) getContext()) / 3; } setMeasuredDimension(sizeWidth, resultHeight); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } private void getHeight(Bitmap bm) { float bitmapWidth = bm.getWidth(); float bitmapHeight = bm.getHeight(); if (bitmapWidth > 0 && bitmapHeight > 0) { float scaleWidth = getWidth() / bitmapWidth; if (scaleWidth != 0) { mHeight = bitmapHeight * scaleWidth; } } } private Bitmap drawableToBitamp(Drawable drawable) { if (drawable != null) { BitmapDrawable bd = (BitmapDrawable) drawable; return bd.getBitmap(); } else { return null; } }}
使用上面的代码之后,我们就完成了图片的完整显示,而且没有任何的性能问题,至此,问题解决。
评论的“楼中楼”、“多楼隐藏”效果实现
在讲解具体实现之前,我们需要先了解一下评论列表的数据结构。
我们以这个测试接口为例:http://jandan.duoshuo.com/api/threads/listPosts.json?thread_key=comment-2750904
因为数据太多,我就不在这里粘贴了,大家自己打开看就可以。
煎蛋使用的是多说的评论接口,所以获取接口都是从多说获取。
从上往下的标签意义如下:
- hotPosts 热门评论
- thread 当前被评论主体的信息,包括thread_id、thread_key、url、comments等重要数据
- cursor 总数和评论页码
- parentPosts 所有的具体评论数据
- response 参与回复的所有主体的id
- options 可选属性,暂时无用
我们需要重点关注的是hotPosts、parentPosts。
在了解我们要显示的数据结构之后,我们就要思考如何去实现我们的评论列表的效果。
第一个问题是,如何添加“热门评论”、“最新评论”的分割标志,并对评论进行分类。
这一步我实在自定义Request里面完成的,为了完成这个跟功能,我们需要一个评论的实体类,下面是重要字段
//评论内容标签 public static final String TAG_HOT = "hot"; public static final String TAG_NORMAL = "normal"; //评论布局类型 public static final int TYPE_HOT = 0; public static final int TYPE_NEW = 1; public static final int TYPE_NORMAL = 2; private String avatar_url; private String created_at; private String name; private String message; //评论发送者id private String post_id; //这条评论所回复的评论id private String parent_id; //这条评论上的所有评论id private String[] parents; //所属楼层 private int floorNum; //用于标示是否是热门评论 private String tag; //用于区别布局类型:热门评论、最新评论、普通评论 private int type;
我们需要内容标签和布局类型,热门评论需要单独筛选出来显示,其他评论按照时间排序,算作最新评论,下面是自定义的Request的实现
/** * Created by zhaokaiqiang on 15/4/10. */public class Request4CommentList extends Request<ArrayList<Commentator>> { private Response.Listener<ArrayList<Commentator>> listener; private LoadFinishCallBack callBack; public Request4CommentList(String url, Response .Listener<ArrayList<Commentator>> listener, Response.ErrorListener errorListener,LoadFinishCallBack callBack) { super(Method.GET, url, errorListener); this.listener = listener; this.callBack = callBack; } @Override protected Response<ArrayList<Commentator>> parseNetworkResponse(NetworkResponse response) { try { //获取到所有的数据 String jsonStr = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); //解析出所有的thread_id,并去掉非法字符,便与解析 JSONObject resultJson = new JSONObject(jsonStr); String allThreadId = resultJson.getString("response").replace("[", "").replace ("]", "").replace("\"", ""); String[] threadIds = allThreadId.split("\\,"); callBack.loadFinish(resultJson.optJSONObject("thread").optString("thread_id")); if (TextUtils.isEmpty(threadIds[0])) { return Response.success(new ArrayList<Commentator>(), HttpHeaderParser .parseCacheHeaders(response)); } else { //然后根据thread_id再去获得对应的评论和作者信息 JSONObject parentPostsJson = resultJson.getJSONObject("parentPosts"); //找出热门评论 String hotPosts = resultJson.getString("hotPosts").replace("[", "").replace ("]", "").replace("\"", ""); String[] allHotPosts = hotPosts.split("\\,"); ArrayList<Commentator> commentators = new ArrayList<>(); List<String> allHotPostsArray = Arrays.asList(allHotPosts); for (String threadId : threadIds) { Commentator commentator = new Commentator(); JSONObject threadObject = parentPostsJson.getJSONObject(threadId); //解析评论,打上TAG if (allHotPostsArray.contains(threadId)) { commentator.setTag(Commentator.TAG_HOT); } else { commentator.setTag(Commentator.TAG_NORMAL); } commentator.setPost_id(threadObject.optString("post_id")); commentator.setParent_id(threadObject.optString("parent_id")); String parentsString = threadObject.optString("parents").replace("[", "").replace ("]", "").replace("\"", ""); String[] parents = parentsString.split("\\,"); commentator.setParents(parents); //如果第一个数据为空,则只有一层 if (TextUtil.isNull(parents[0])) { commentator.setFloorNum(1); } else { commentator.setFloorNum(parents.length + 1); } commentator.setMessage(threadObject.optString("message")); commentator.setCreated_at(threadObject.optString("created_at")); JSONObject authorObject = threadObject.optJSONObject("author"); commentator.setName(authorObject.optString("name")); commentator.setAvatar_url(authorObject.optString("avatar_url")); commentator.setType(Commentator.TYPE_NORMAL); commentators.add(commentator); } return Response.success(commentators, HttpHeaderParser.parseCacheHeaders(response)); } } catch (Exception e) { e.printStackTrace(); return Response.error(new ParseError(e)); } } @Override protected void deliverResponse(ArrayList<Commentator> response) { listener.onResponse(response); }}
我们在parseNetworkResponse方法里面完成了所有的数据解析,并且将热门评论打上tag区分开来,同时根据parents字段对应的数组长度,判断出当前楼层,至此,我们的数据就准备好了。
那么,解析完数据之后,应该怎么做呢?
我们来看一下请求完之后的回调做了什么。
在Adapter中,我封装了加载数据的方法loadData()。
public void loadData() { executeRequest(new Request4CommentList(Commentator.getUrlCommentList(thread_key), new Response .Listener<ArrayList<Commentator>>() { @Override public void onResponse(ArrayList<Commentator> response) { google_progress.setVisibility(View.GONE); tv_error.setVisibility(View.GONE); if (response.size() == 0) { tv_no_thing.setVisibility(View.VISIBLE); } else { commentators.clear(); ArrayList<Commentator> hotCommentator = new ArrayList<>(); ArrayList<Commentator> normalComment = new ArrayList<>(); //添加热门评论 for (Commentator commentator : response) { if (commentator.getTag().equals(Commentator.TAG_HOT)) { hotCommentator.add(commentator); } else { normalComment.add(commentator); } } //添加热门评论标签 if (hotCommentator.size() != 0) { Collections.sort(hotCommentator); Commentator hotCommentFlag = new Commentator(); hotCommentFlag.setType(Commentator.TYPE_HOT); hotCommentator.add(0, hotCommentFlag); commentators.addAll(hotCommentator); } //添加最新评论及标签 if (normalComment.size() != 0) { Commentator newCommentFlag = new Commentator(); newCommentFlag.setType(Commentator.TYPE_NEW); commentators.add(newCommentFlag); Collections.sort(normalComment); commentators.addAll(normalComment); } mAdapter.notifyDataSetChanged(); } mSwipeRefreshLayout.setRefreshing(false); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { mSwipeRefreshLayout.setRefreshing(false); google_progress.setVisibility(View.GONE); tv_error.setVisibility(View.VISIBLE); tv_no_thing.setVisibility(View.GONE); } }, new LoadFinishCallBack() { @Override public void loadFinish(Object obj) { thread_id = (String) obj; } })); } }
在这段代码里面,我们先清空了commentators,这个集合里面将防止我们处理好的数据。我创建了hotCommentator和normalComment两个集合,分别用来存放热门评论和一般评论。整个评论列表是通过RecycleView来做的,我们都知道ListView支持多种布局类型,RecycleView也一样,我们可以根据hotCommentator和normalComment这两个集合的长度来决定是否添加热门评论和最新评论是否显示,如果显示的话,添加一个设置好Type的Commentator对象即可。由于需要使用Collections.sort()进行排序,所以我们的实体类需要实现Comparable接口,然后根据发布时间排序
@Override public int compareTo(Object another) { String anotherTimeString = ((Commentator) another).getCreated_at().replace("T", " "); anotherTimeString = anotherTimeString.substring(0, anotherTimeString.indexOf("+")); String thisTimeString = getCreated_at().replace("T", " "); thisTimeString = thisTimeString.substring(0, thisTimeString.indexOf("+")); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); simpleDateFormat.setTimeZone(TimeZone.getTimeZone("GMT+08")); try { Date anotherDate = simpleDateFormat.parse(anotherTimeString); Date thisDate = simpleDateFormat.parse(thisTimeString); return -thisDate.compareTo(anotherDate); } catch (ParseException e) { e.printStackTrace(); return 0; } }
那么怎么实现多种布局呢?
首先,需要实现getItemViewType方法,如下
@Overridepublic int getItemViewType(int position) { return commentators.get(position).getType();}
设置好ViewType之后,我们在onCreateViewHolder里面就可以根据viewType生成ViewHolder了
@Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { switch (viewType) { case Commentator.TYPE_HOT: case Commentator.TYPE_NEW: return new ViewHolder(getLayoutInflater().inflate(R.layout .item_comment_flag, parent, false)); case Commentator.TYPE_NORMAL: return new ViewHolder(getLayoutInflater().inflate(R.layout.item_comment, parent, false)); default: return null; } }
需要注意的是,我们在创建我们自己的ViewHolder的时候,需要把所有布局里面用到的View都绑定好,如下
private static class ViewHolder extends RecyclerView.ViewHolder { private TextView tv_name; private TextView tv_content; private TextView tv_time; private LinearLayout ll_vote; private SimpleDraweeView img_header; private FloorView floors_parent; private TextView tv_flag; public ViewHolder(View itemView) { super(itemView); tv_name = (TextView) itemView.findViewById(R.id.tv_name); tv_content = (TextView) itemView.findViewById(R.id.tv_content); tv_time = (TextView) itemView.findViewById(R.id.tv_time); ll_vote = (LinearLayout) itemView.findViewById(R.id.ll_vote); img_header = (SimpleDraweeView) itemView.findViewById(R.id.img_header); floors_parent = (FloorView) itemView.findViewById(R.id.floors_parent); tv_flag = (TextView) itemView.findViewById(R.id.tv_flag); setIsRecyclable(false); } }
其实tv_flag在正常布局里面没有这个TextView,只存在于评论的分割布局里面,但是我们同样需要在这里find出来,否则没法使用。
至于这个setIsRecyclable(false)则是设置当前的ViewHolder不能够复用,因为在这里复用会导致布局混乱,不复用肯定会效率低一些,但是我还没找到其他好的解决方案。
这些工作做完之后,我们就需要在onBindViewHolder里面进行数据绑定了。
@Override public void onBindViewHolder(ViewHolder holder, int position) { final Commentator commentator = commentators.get(position); switch (commentator.getType()) { case Commentator.TYPE_HOT: holder.tv_flag.setText("热门评论"); break; case Commentator.TYPE_NEW: holder.tv_flag.setText("最新评论"); break; case Commentator.TYPE_NORMAL: holder.tv_name.setText(commentator.getName()); holder.tv_content.setText(commentator.getMessage()); ... //有楼层,盖楼 if (commentator.getFloorNum() > 1) { SubComments cmts = new SubComments(addFloors(commentator)); holder.floors_parent.setComments(cmts); holder.floors_parent.setFactory(new SubFloorFactory()); holder.floors_parent.setBoundDrawer(getResources().getDrawable( R.drawable.bg_comment)); holder.floors_parent.init(); } else { holder.floors_parent.setVisibility(View.GONE); } ... break; } }
为了更清晰,我在中间省去了很多代码,前两个case就是填充我们的评论类型分割布局,第三个case则是真正的评论数据的填充,在这里我们就实现了“楼中楼”和“多楼隐藏”效果。
在开始正式介绍之前,我简单的介绍下实现的思路。
首先,我们完成这个效果,需要自定义一个Linearlayout,当只有一层楼时,我们隐藏它,如果有盖楼效果,我们需要把所有的楼层放到这个LinearLayout里面,评论内容放在TextView里面,楼层的外框需要我们单独画出。
下面是评论布局的xml文件
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:fresco="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="16.0dip"> <LinearLayout android:id="@+id/left" android:layout_width="56.0dip" android:layout_height="wrap_content" android:layout_marginLeft="16.0dip" android:orientation="vertical" > <com.facebook.drawee.view.SimpleDraweeView android:id="@+id/img_header" android:layout_width="40dp" android:layout_height="40dp" fresco:roundedCornerRadius="5dp" fresco:placeholderImage="@drawable/ic_loading_small" /> </LinearLayout> <RelativeLayout android:id="@+id/right" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_toEndOf="@id/left" android:layout_toRightOf="@id/left" android:paddingEnd="16.0dip" android:paddingLeft="0.0dip" android:paddingRight="16.0dip" android:paddingStart="0.0dip"> <View android:id="@+id/left_placeholder" android:layout_width="16.0dip" android:layout_height="1.0dip" android:visibility="visible"/> <TextView android:id="@+id/tv_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/left_placeholder" android:ellipsize="end" android:singleLine="true" android:text="AAAAAAAAAA" android:maxLength="10" android:textColor="@color/primary_text_default_material_light" android:textSize="15.0sp" android:textStyle="bold"/> <TextView android:id="@+id/tv_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@id/tv_name" android:layout_marginLeft="8.0dip" android:layout_toRightOf="@id/tv_name" android:text="2 mins ago" android:textColor="@color/secondary_text_default_material_light" android:textSize="13.0sp" android:visibility="visible"/> <com.socks.jiandan.view.floorview.FloorView android:id="@+id/floors_parent" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_toRightOf="@id/left_placeholder" android:layout_marginTop="8dp" android:layout_below="@id/tv_name" android:background="@drawable/bg_floor" /> <TextView android:id="@+id/tv_content" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_below="@id/floors_parent" android:layout_marginTop="8dp" android:layout_toRightOf="@id/left_placeholder" android:lineSpacingExtra="4dp" android:text="aaa" android:textColor="@color/primary_text_default_material_light" android:textSize="14sp"/> <LinearLayout android:id="@+id/ll_vote" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBottom="@id/tv_name" android:layout_alignParentRight="true" android:gravity="right" android:orientation="horizontal" android:visibility="visible"> <LinearLayout android:id="@+id/support" android:layout_width="wrap_content" android:layout_height="fill_parent" android:orientation="horizontal"> <TextView android:id="@+id/like_descr" android:layout_width="wrap_content" android:layout_height="fill_parent" android:gravity="center" android:text="OO " android:textColor="@color/secondary_text_default_material_light" android:textSize="13.0sp"/> <TextView android:id="@+id/like" android:layout_width="wrap_content" android:layout_height="fill_parent" android:gravity="center" android:textColor="@color/secondary_text_default_material_light" android:textSize="13.0sp" android:textStyle="normal"/> </LinearLayout> <LinearLayout android:id="@+id/unsupport" android:layout_width="wrap_content" android:layout_height="fill_parent" android:orientation="horizontal"> <TextView android:id="@+id/unlike_descr" android:layout_width="wrap_content" android:layout_height="fill_parent" android:gravity="center" android:text=" XX " android:textColor="@color/secondary_text_default_material_light" android:textSize="13.0sp"/> <TextView android:id="@+id/unlike" android:layout_width="wrap_content" android:layout_height="fill_parent" android:gravity="center" android:textColor="@color/secondary_text_default_material_light" android:textSize="13.0sp" android:textStyle="normal"/> </LinearLayout> </LinearLayout> </RelativeLayout> <View android:id="@+id/divider" android:layout_width="fill_parent" android:layout_height="1.0px" android:layout_below="@id/right" android:layout_marginTop="16.0dip" android:layout_toEndOf="@id/left" android:layout_toRightOf="@id/left" android:background="#ffd9d9d9"/></RelativeLayout>
tv_content是评论内容,floors_parent则是我们自定义的控件,我们重点看下这个是如何实现的。
完成整个盖楼功能,需要三个类,如下
- FloorView 自定义LinearLayout,完成楼层的内容填充、分割线绘制和布局添加
- SubComments 对评论的再次封装,每一个评论都对应一个SubComments对象,它里面封装个这个评论的所有楼层内容
- SubFloorFactory Floor工厂,用于根据不同类型产生正常评论View和隐藏楼层View,同时在产生过程中,完成了数据和View的适配
介绍完这三个类,我们看下用法
SubComments cmts = new SubComments(addFloors(commentator)); holder.floors_parent.setComments(cmts); holder.floors_parent.setFactory(new SubFloorFactory()); holder.floors_parent.setBoundDrawer(getResources().getDrawable( R.drawable.bg_comment)); holder.floors_parent.init();
上面的代码完整的展示了调用的流程。
首先生成一个SubComments数据封装对象,这里调用了一个addFloors方法,代码如下
private List<Commentator> addFloors(Commentator commentator) { //只有一层 if (commentator.getFloorNum() == 1) { return null; } List<String> parentIds = Arrays.asList(commentator.getParents()); List<Commentator> commentators = new ArrayList<>(); for (Commentator comm : this.commentators) { if (parentIds.contains(comm.getPost_id())) { commentators.add(comm); } } Collections.reverse(commentators); return commentators; }
在addFloors里面其实我们就完成了一件事,那就是把当前commentator对象的所有父级对象都找出来,然后添加进集合后按时间排序,这样我们就能获取到一条评论的所有信息啦~
之后,我们又setComments、setFactory、setBoundDrawer,全部设置齐活,调用init()就出来了~
那么init到底做了些什么?下面是FloorView源码
/** * @author JohnnyShieh * @ClassName: FloorView * @Description: * @date Jan 25, 2014 2:09:36 PM */@TargetApi(Build.VERSION_CODES.HONEYCOMB)public class FloorView extends LinearLayout { private int density; private Drawable drawer; private SubComments datas; private SubFloorFactory factory; public FloorView(Context context) { super(context); init(context); } public FloorView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public FloorView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } public void setBoundDrawer(Drawable drawable) { drawer = drawable; } public void setComments(SubComments cmts) { datas = cmts; } public void setFactory(SubFloorFactory fac) { factory = fac; } public int getFloorNum() { return getChildCount(); } private void init(Context context) { this.setOrientation(LinearLayout.VERTICAL); density = (int) (3.0F * context.getResources().getDisplayMetrics().density); } public void init() { if (null == datas.iterator()) return; if (datas.getFloorNum() < 7) { for (Iterator<Commentator> iterator = datas.iterator(); iterator .hasNext(); ) { View view = factory.buildSubFloor(iterator.next(), this); addView(view); } } else { View view; view = factory.buildSubFloor(datas.get(0), this); addView(view); view = factory.buildSubFloor(datas.get(1), this); addView(view); view = factory.buildSubHideFloor(datas.get(2), this); view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { TextView hide_text = (TextView) v .findViewById(R.id.hide_text); hide_text.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); v.findViewById(R.id.hide_pb).setVisibility(View.VISIBLE); removeAllViews(); for (Iterator<Commentator> iterator = datas.iterator(); iterator .hasNext(); ) { View view = factory.buildSubFloor(iterator.next(), FloorView.this); addView(view); } reLayoutChildren(); } }); addView(view); view = factory.buildSubFloor(datas.get(datas.size() - 1), this); addView(view); } reLayoutChildren(); } public void reLayoutChildren() { int count = getChildCount(); for (int i = 0; i < count; i++) { View view = getChildAt(i); LayoutParams layout = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); layout.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; int margin = Math.min((count - i - 1), 4) * density; layout.leftMargin = margin; layout.rightMargin = margin; if (i == count - 1) { layout.topMargin = 0; } else { layout.topMargin = Math.min((count - i), 4) * density; } view.setLayoutParams(layout); } } @Override protected void dispatchDraw(Canvas canvas) { int i = getChildCount(); if (null != drawer && i > 0) { for (int j = i - 1; j >= 0; j--) { View view = getChildAt(j); drawer.setBounds(view.getLeft(), view.getLeft(), view.getRight(), view.getBottom()); drawer.draw(canvas); } } super.dispatchDraw(canvas); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (this.getChildCount() <= 0) { setMeasuredDimension(0, 0); return; } super.onMeasure(widthMeasureSpec, heightMeasureSpec); }}
可以看到,在init()首先根据评论数量判断,是否隐藏,不隐藏,就调用SubFloorFactory的buildSubFloor创建一个楼层,要是隐藏呢?就创建一楼、二楼,然后创建一个隐藏楼层,然后创建最后一个楼层。创建完之后调用reLayoutChildren()。
public void reLayoutChildren() { int count = getChildCount(); for (int i = 0; i < count; i++) { View view = getChildAt(i); LayoutParams layout = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); layout.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL; int margin = Math.min((count - i - 1), 4) * density; layout.leftMargin = margin; layout.rightMargin = margin; if (i == count - 1) { layout.topMargin = 0; } else { layout.topMargin = Math.min((count - i), 4) * density; } view.setLayoutParams(layout); } }
在这里面,根据不同的楼层,设置不同的margin,从而显示出一层挨着一层的效果。
那么每一层的间隔线呢?是在dispatchDraw()里面实现的
@Override protected void dispatchDraw(Canvas canvas) { int i = getChildCount(); if (null != drawer && i > 0) { for (int j = i - 1; j >= 0; j--) { View view = getChildAt(j); drawer.setBounds(view.getLeft(), view.getLeft(), view.getRight(), view.getBottom()); drawer.draw(canvas); } } super.dispatchDraw(canvas); }
通过重写dispatchDraw(),在画childView之前,先把边框绘制出来,这样就实现了边框效果。注意绘制顺序,super.dispatchDraw(canvas)需要在最后调用,否则会覆盖。
如果存在隐藏楼层,怎么点击全部显示出来呢?
view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { TextView hide_text = (TextView) v .findViewById(R.id.hide_text); hide_text.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0); v.findViewById(R.id.hide_pb).setVisibility(View.VISIBLE); removeAllViews(); for (Iterator<Commentator> iterator = datas.iterator(); iterator .hasNext(); ) { View view = factory.buildSubFloor(iterator.next(), FloorView.this); addView(view); } reLayoutChildren(); } });
在点击之后,首先removeAllViews(),然后创建了新的view,使用addView添加进去,最后reLayoutChildren()就可以了。
至此,“盖楼”效果就完全实现了。
因为文章太长了,所以剩下的内容只能放到下一篇了,写的好累呀,休息下~
别忘记去项目star一下哦
项目地址:https://github.com/ZhaoKaiQiang/JianDan
- 【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(二)——大图显示模式、评论“盖楼”效果实现详解
- 【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(二)——大图显示模式、评论“盖楼”效果实现详解
- 【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(二)——大图显示模式、评论“盖楼”效果实现详解
- 【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(三)——使用GreenDao实现本地Sqlite缓存
- 【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(一)——逆向工程及TcpDump抓包入门
- 【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(一)——逆向工程及TcpDump抓包入门
- “煎蛋”Android版的高仿及优化(一)——逆向工程及TcpDump抓包入门
- Android网易评论盖楼效果实现
- Android网易评论盖楼效果实现
- Android网易新闻评论盖楼效果的实现
- Android网易新闻评论盖楼效果的实现
- 仿网易新闻评论“盖楼”效果实现
- 【凯子哥带你学Android】Andriod性能优化之列表卡顿——以“简书”APP为例
- “煎蛋”Android版的高仿GitHub路径
- Android 使用ListView实现网易评论盖楼效果
- 【凯子哥带你夯实应用层】新手必备的常用代码片段整理(二)
- 【凯子哥带你夯实应用层】新手必备的常用代码片段整理(二)
- 【凯子哥带你夯实应用层】新手必备的常用代码片段整理(二)
- Zend Studio代码无法自动提示以及代码跟踪函数和变量问题的解决方法
- C++之泛型编程笔记
- JS-HTML基本标签
- 进程隐藏与进程保护(SSDT Hook 实现)(二) 转载自 Zachary.XiaoZhen - 梦想的天空
- org.hibernate.MappingException: Unknown entity常见问题(新手需注意)
- 【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(二)——大图显示模式、评论“盖楼”效果实现详解
- 日经社説 20150427 「大阪都」巡る住民投票の意味
- 实时统计各进程的流量
- 进程隐藏与进程保护(SSDT Hook 实现)(三) 转载自 Zachary.XiaoZhen - 梦想的天空
- eclipse中maven配置
- AJAX使用出现乱码的问题
- 日经社説 20150427 日本のITはウエアラブルで巻き返せ
- Android 服务中显示Notification失败。
- 事件分发机制