转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992
在前一篇文章中,我们学习了如何进行逆向工程和TcpDump进行抓包,获取我们的数据接口,那么有了数据之后,我们就可以开始代码编写工作了。
本项目在前几天获得了daimajia大神的推荐,star数已经达到115,多谢大家的支持,欢迎提建议和意见。
项目地址:https://github.com/ZhaoKaiQiang/JianDan
目前项目已完成以下功能,本文章将会总结在编码过程中遇到的挑战和解决方案。
从上面的效果图也可以看出来,我们使用的是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。原因如下:
在IOC框架的选择上,使用butter knife,之前一直使用AFinal,但是AFinal属于运行期绑定,会影响性能,butter knife属于编译期绑定,不会影响。使用butter knife使用非常方便,就拿来一用。在本项目中,我感觉其实并不是很需要IOC,仅作一个尝试而已,不必深究。
在完成网络状态切换的功能上,需要在MainActivity注册一个网络状态监听器,当网络状态发生改变的时候,通知当前显示的Fragment切换图片的加载模式,或者是提示网络状态变化情况。在这种需求下,使用接口是可以完成的,每个Fragment都实现MainActivity的一个接口,当网络状态发生变化的时候,MainActivity调用Fragment的接口方法即可。但是这样不仅很麻烦,而且会增加耦合性,为此,我使用EventBus完成了这个功能,实现很简单,大家看源码就可以,耦合度为0。
这个项目中的所有数据接口基本都是Json格式,所以选择一个好的解析框架是很重要的。我之前写过三篇文章介绍了Json的不同解析方法,虽然Jackson的解析速度快,但是gson确实用起来很熟悉,而且我们要解析的数据量并不大,性能上的差异微乎其微,所以我选择了我比较熟悉的gson。在解析的一些地方还用到了一些JSON,这个大家可以自由选择。
我们在前面介绍Fresco的时候提到过,之所以放弃它,很大的一个原因就是因为这个功能它不支持,我们先来看看我们要实现功能的详细分析。
也就是下面的效果
我的解决思路是这样的,宽度和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、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方法,如下
@Override
public 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则是我们自定义的控件,我们重点看下这个是如何实现的。
完成整个盖楼功能,需要三个类,如下
介绍完这三个类,我们看下用法
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一下哦
【凯子哥带你做高仿】“煎蛋”Android版的高仿及优化(二)——大图显示模式、评论“盖楼”效果实现详解
原文地址:http://blog.csdn.net/zhaokaiqiang1992/article/details/45306313