转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992
在前一篇文章中,我们学习了如何进行逆向工程和TcpDump进行抓包,获取我们的数据接口,那么有了数据之后,我们就可以开始代码编写工作了。
本项目在前几天获得了daimajia大神的推荐,star数已经达到115,多谢大家的支持,欢迎提建议和意见。
项目地址:https://github.com/ZhaoKaiQiang/JianDan
目前项目已完成以下功能,本文章将会总结在编码过程中遇到的挑战和解决方案。
项目进度
已完成的功能
- 查看段子
- 查看无聊图(静态图和GIF动态图)
- 查看妹子图(程序员必备)
- 对段子、无聊图、妹子图进行投票
- 段子的复制与分享
- 无聊图、妹子图的保存与分享
- 查看吐槽与回复
- 图片详情页的动画效果
- 添加新鲜事列表页
- 添加新鲜事详情页
优化的功能
- 添加加载等待动画
- 添加加载失败提示
- 添加段子列表界面,点击标题栏快速返回顶端
- 添加评论楼层过多隐藏
- 添加网络状态检测
- 优化无聊图列表显示,非WIFI状态下,显示GIF缩略图,点击后下载
- 加载模式全自动智能切换,显著提高加载速度,节省大量流量
- 修改图片详情页为完全沉浸效果
- 图片详情页添加投票结果的颜色标示
- 添加图片列表滚动检测,滚动状态暂停加载,进一步提高加载速度,减少卡顿
- 添加图片加载图片
- 添加当前栏目标志,避免重复切换
- 修改新鲜事列表页效果为CardView
想完成但没有完成的功能
- 列表加载动画(虽然试过好多次,但是都不能实现首次加载,CardView进入时的动画效果,如果你能知道我如何实现,我将非常感激)
- 本地缓存(后期将添加)
效果图
项目整体架构介绍
使用的开源框架
项目整体介绍
从上面的效果图也可以看出来,我们使用的是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,这个大家可以自由选择。
项目中遇到的问题及解决方案
加载任意高度的图片
我们在前面介绍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