项目地址:https://github.com/razerdp/FriendCircle
一起撸个朋友圈吧这是本文所处文集,所有更新都会在这个文集里面哦,欢迎关注
上篇链接:http://www.jianshu.com/p/15a9fe8f917f
下篇链接:http://www.jianshu.com/p/58894dfb3f09
本篇图文较多,流量党请慎重
再开始之前,羽翼君想说一件事情,目前我还是一个学生,没什么能力买一个牛逼的服务器,仅仅是学生价租的阿里云,但是,因为为了方便,我把服务器的IP都写在了我们的项目里面,结果他喵的昨晚被攻击了!
这个服务器根本没有什么利用价值,即使是用来做肉鸡,根本没法塞牙缝好么。虽然花的钱不多,也仅仅是为了这个项目和我的毕业设计方便而租的服务器。
当然,这也是我的错,我不应该直接把服务器地址贴上的,这也是我的锅,所以在push的时候我把地址换成了我的吐槽。。。
如果您需要测试数据,您可以简信我或者加我的QQ来拿到地址,非常抱歉我这么做。(我很害怕到毕业设计答辩那天来个攻击啊)
【END】
在上篇,我们初步完成了评论popup的展示,这一次我们需要补全剩下的交互代码。
首先上预览图吧(为了方便,以后统一在电脑模拟器上录制):
<h1 id="step1">Step 1:困境</h1>
在实现之前,不妨看看我们现在遇到的问题:
如下图:
从图中我们可以看到,我们现在整一个朋友圈的实现方案如下:
- Activity作为一个controller,它现在仅仅负责的是拉取数据,并没有其他的工作。
- Adapter,在我们将viewholder抽象出来后,adapter看起来仅仅就是将类型跟对应的viewholder匹配起来,并渲染出来。
- 而ViewHolder,则是负责将数据展示,同时一切的数据/操作都是在ViewHolder实现的。
那么问题来了,我们的工程进行到这里,我们其实没有做任何的点击/请求(朋友圈列表拉取除外)而如今,我们需要增加交互等方法,按照图中的结构,我们可以有如下的方法(目前我所想到的):
- 因为viewholder持有activity的context,我们可以通过activity提供公用方法,然后使用(if context instance of xxx){ (Activity)context.xxxx}来调用activity的方法
- EventBus事件通知
- 中间类,使用中间类来处理activity与viewholder之间的交互。
显然,方法一过于笨重不便于扩展,方法二虽然挺方便的,但是在onEventMainThread方法里我们需要很多的判断,所以我们使用方法三。
那么这个中间类是干什么的呢?直观的说,就是如下图这样的结构:
可能看图还是有点不太明白,那我们通过一个例子来解释一下吧:
假如故事发生在一个初创公司,这个公司目前的分层如下:
- BOSS(对应
Activity
) - 技术总监CTO(对应
Adapter
) - 具体各个技术小组的leader(对应各个
ViewHolder
)
在开始阶段因为急需要做出产品给投资人看效果,所以并没有招到很多人,因此一直都是Boss下发需求给CTO,然后CTO评估后再下发给技术小组的leader,然后leader完成需求。
(类比于我们撸朋友圈目前进度:先完成界面展示,而不管任何交互)*
在前期,这样做问题不大,OK,这个初创公司顺利的拿下A轮投资,接下来B轮就需要打造产品特点和细节研磨,这时候就会发现,如果还是按照之前的做法(boss->cto->leader->产品生产),效率大大的降低,同时因为leader忙着忙那,同时应对着boss变来变去的需求,在自己负责的区域应对着一波又一波的需求忙得焦头烂额。(viewholder与activity耦合度过高)
于是,他们决定请人。经过简历筛选,笔试,面试后,他们找到了合适的人选,于是接下来的分工就变成了这样:
- boss下发需求(此时其实应该是boss跟产品评估,但为了篇幅,先略过产品)(Activity通知adapter更新)
- CTO开评估会议并细分/下发任务(Adapter将数据分发到各个viewholder并渲染)
- leader收到任务,开内部会议进行分工,而leader则是负责项目结构,项目基础框架的优化等(viewholder绑定数据并展示)
- 各个小组成员收到任务,开始投入生产(码代码)(controll处理各种交互,请求等事件)
然后当各个小组成员完成任务,交由给leader,leader进行review后交由测试,测试通过后可以通知可以准备发版。在各个高层使用过初步满意后正式发版。(在我们的代码里,省略那么多步骤,直接通知activity进行更新)
说了那么多,其实就是一句话:controller承担了最繁琐的步骤,做好后通知activity去更新数据。
<h1 id="step2">Step 2:controller的实现</h1>
在一大篇无聊的叙述后,我们就谈谈如何实现controller。谈起controller,就不得不想到MVC,进而想到MVP。我们这里并非实现MVP,但总的来说,有点形似而神不似吧。
首先既然要做解耦,那就必须涉及到抽象,而抽象,就我经验来说,接口化应该是最好的。
所以我们先抽象出一个BaseController(注:此接口不遵循单一职责原则):
/**
* Created by 大灯泡 on 2016/3/9.
* 控制器接口化
*/
public interface BaseDynamicController {
// 点赞
void addPraise(long userid, long dynamicid, MomentsInfo info, @RequestType.DynamicRequestType int requesttype);
// 取消点赞
void cancelPraise(long userid, long dynamicid, MomentsInfo info, @RequestType.DynamicRequestType int requesttype);
}
我们需要的操作都将会在接口里限定。
关于@RequestType.DynamicRequestType
,一般而言,在需要传入一定范围内的值时,我们应该使用注解限定,这样可以降低误操作。另外RequestType用于区分同一个类下多个请求。
所以这里限定传入的值必须是DynamicRequestType所支持的值:
public class RequestType {
@Retention(RetentionPolicy.SOURCE)
@IntDef({ADD_PRAISE,CANCEL_PRAISE})
public @interface DynamicRequestType{}
// 点赞
public static final int ADD_PRAISE=0x10;
public static final int CANCEL_PRAISE=0x11;
}
目前我们只做了点赞和取消点赞,所以暂时只需要这两个类型。
接口写完后,我们接下来需要实现这个接口,定义一个类DynamicController并实现请求回调接口以及刚刚我们定义的controller接口:
/**
* Created by 大灯泡 on 2016/3/8.
* 事件控制器
* 本控制器用于BaseItemDelegate的事件处理
* 事件处理完成通过callback回调给activity,避免BaseItem与activity耦合度过高
*/
public class DynamicController implements BaseResponseListener, BaseDynamicController {
private static final String TAG = "DynamicController";
private CallBack mCallBack;
private Activity mContext;
//=============================================================request
private DynamicAddPraiseRequest mDynamicAddPraiseRequest;
private DynamicCancelPraiseRequest mDynamicCancelPraiseRequest;
public DynamicController(Activity context, @NonNull CallBack callBack) {
mContext = context;
mCallBack = callBack;
}
//=============================================================request callback
@Override
public void onStart(BaseResponse response) {
}
@Override
public void onStop(BaseResponse response) {
}
@Override
public void onFailure(BaseResponse response) {
}
@Override
public void onSuccess(BaseResponse response) {
}
//=============================================================controller methods
@Override
public void addPraise(long userid, long dynamicid, MomentsInfo info,
@RequestType.DynamicRequestType int requesttype) {
}
@Override
public void cancelPraise(long userid, long dynamicid, MomentsInfo info,
@RequestType.DynamicRequestType int requesttype) {
}
//=============================================================destroy
public void destroyController() {
}
public interface CallBack {
void onResultCallBack(BaseResponse response);
}
}
在处理完成后,我们需要通知activity进行数据更新,所以我们需要定一个CallBack,让activity实现这个接口。同时为了紧张的内存,我们还需要定义一个destroy方法,及时的进行对象置空。
接下来改造一下我们的BaseItemDelegate,因为我们当初设计的时候是采取接口的形式,所以我们实质上是改造BaseItemView
public interface BaseItemView<T> {
...
void setController(BaseDynamicController controller);
BaseDynamicController getController();
}
我们在BaseItemView添加controller的setter/getter,然后在BaseItemDelegate进行赋值,最后就到我们的Adapter进行设置:
CircleBaseAdapter.java:
//因为我们当初设计的时候采用的builder模式,所以我们仅仅需要到builder添加一个参数就好了,这里就不贴代码了。
public CircleBaseAdapter(Activity context, Builder<T> mBuilder) {
...
mDynamicController=mBuilder.mDynamicController;
}
...
@Override
public View getView(int position, View convertView, ViewGroup parent) {
...
view.setActivityContext(context);
view.onFindView(convertView);
view.onBindData(position, convertView, getItem(position), dynamicType);
if (view.getController()==null)view.setController(mDynamicController);
return convertView;
}
因为重复的代码在之前的简书都有记录,所以这里就略过了,如果您看的云里雾里,可以在这篇文章看到所有的解析。
在viewholder和controller完成后,我们最后需要在activity将controller给new出来,然后添加到builder里面,使adapter,activity共同持有一个对象。
public class FriendCircleDemoActivity extends FriendCircleBaseActivity implements DynamicController.CallBack {
private FriendCircleRequest mCircleRequest;
private DynamicController mDynamicController;
// 方案二,预留
/* @Override
protected void onEventMainThread(Events events) {
if (events == null || events.getEvent() == null) return;
if (events.getEvent() instanceof Events.CallToRefresh) {
if (((Events.CallToRefresh) events.getEvent()).needRefresh) mCircleRequest.execute();
}
}*/
@Override
protected void onCreate(Bundle savedInstanceState) {
...
bindListView(R.id.listview, header,
FriendCircleAdapterUtil.getAdapter(this, mMomentsInfos, mDynamicController));
initReq();
//mListView.manualRefresh();
}
...
@Override
public void onResultCallBack(BaseResponse response) {
}
...
}
其中FriendCircleAdapterUtil的代码略过,详情可以看GitHub。
到这里为止,我们的结构大致完成,接下来就是将剩下的代码补全。
<h1 id="step3">Step 3:代码补全</h1>
首先我们思考一下,如何更新我们的界面是最好的。目前来说我想到有以下方法:
- 当点赞/取消点赞请求成功后,我们调用activity的列表请求将整个朋友圈数据都重新拉一遍。
- 当点赞/取消点赞请求成功后,服务器返回当前动态的点赞列表信息,我们获取当前动态的实体类,解析服务器返回信息后进行更新操作。
- 当点赞/取消点赞请求成功后,本地进行插入/删除。
上面几个方案中
第一个方案很明显是不适合的,因为每次点赞都需要将整个列表拉一次,解析耗时不说,就流量消耗也是很可观的。遂放弃。
第二个方案目前采用,但也有不完善的地方,这个下文再说。
第三个方案看似不错,但如果遇到下面这种情况就不适应了:你点赞的同时你好友也点赞,但因为没有数据返回,所以你好友的点赞并没有刷出来。
综上所述,目前采取第二个方案。(ps:第二个方案目前我的实现并不好,但暂时没有想到更好的方案,如果您有好的建议,在下衷心希望可以留下您的评论)
首先回到我们的controller中,在CallBack里我们传递的参数是BaseResponse,但BaseResponse当初我们设计的时候,其接受的数据如下:
public class BaseResponse {
//请求码
private int status;
//错误码
private int errorCode;
//请求类型,用于单activity多个请求的区分
private int requestType;
//请求回来的JSON字符串
private String jsonStr;
//错误信息
private String errorMsg;
//待用,可以存放解析后的JSON Array
private ArrayList<Object> datas=new ArrayList<>();
//存放解析后的数据
private Object data;
//是否展示dialog
private boolean showDialog;
private int start;
private boolean hasMore;
...
}
可以看到,我们存放数据的地方仅仅只有一个Object,但我们需要拿到一个动态实体和点赞后服务器返回的数据解析。当然,我们可以选择在BaseResponse再添加一个对象来存放,但如果这样做,就会导致以后这个类也许会越来越臃肿。而这并不是我所希望看到的。
所以,我们需要开辟一个新的用于controller的实体:
/**
* Created by 大灯泡 on 2016/3/10.
* 控制器实体类
*/
public class DynamicControllerEntity<T> {
private MomentsInfo mMomentsInfo;
private T data;
public MomentsInfo getMomentsInfo() {
return mMomentsInfo;
}
public void setMomentsInfo(MomentsInfo momentsInfo) {
mMomentsInfo = momentsInfo;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
做好这个之后,我们补全controller的内容:
/**
* Created by 大灯泡 on 2016/3/8.
* 事件控制器
* 本控制器用于BaseItemDelegate的事件处理
* 事件处理完成通过callback回调给activity,避免BaseItem与activity耦合度过高
*/
public class DynamicController implements BaseResponseListener, BaseDynamicController {
private static final String TAG = "DynamicController";
private CallBack mCallBack;
private Activity mContext;
//=============================================================request
private DynamicAddPraiseRequest mDynamicAddPraiseRequest;
private DynamicCancelPraiseRequest mDynamicCancelPraiseRequest;
public DynamicController(Activity context, @NonNull CallBack callBack) {
mContext = context;
mCallBack = callBack;
}
//=============================================================request callback
...
@Override
public void onSuccess(BaseResponse response) {
if (response.getStatus() == 200) {
if (mCallBack != null) mCallBack.onResultCallBack(response);
}
else {
ToastUtils.ToastMessage(mContext, response.getErrorMsg());
}
}
//=============================================================controller methods
@Override
public void addPraise(long userid, long dynamicid, MomentsInfo info,
@RequestType.DynamicRequestType int requesttype) {
if (mDynamicAddPraiseRequest == null) {
mDynamicAddPraiseRequest = new DynamicAddPraiseRequest(info);
mDynamicAddPraiseRequest.setOnResponseListener(this);
mDynamicAddPraiseRequest.setRequestType(requesttype);
}
mDynamicAddPraiseRequest.setInfo(info);
mDynamicAddPraiseRequest.userid = userid;
mDynamicAddPraiseRequest.dynamicid = dynamicid;
mDynamicAddPraiseRequest.execute();
}
@Override
public void cancelPraise(long userid, long dynamicid, MomentsInfo info,
@RequestType.DynamicRequestType int requesttype) {
if (mDynamicCancelPraiseRequest == null) {
mDynamicCancelPraiseRequest = new DynamicCancelPraiseRequest(info);
mDynamicCancelPraiseRequest.setOnResponseListener(this);
mDynamicCancelPraiseRequest.setRequestType(requesttype);
}
mDynamicCancelPraiseRequest.setInfo(info);
mDynamicCancelPraiseRequest.userid = userid;
mDynamicCancelPraiseRequest.dynamicid = dynamicid;
mDynamicCancelPraiseRequest.execute();
}
//=============================================================destroy
public void destroyController() {
mDynamicAddPraiseRequest = null;
mCallBack = null;
}
public interface CallBack {
void onResultCallBack(BaseResponse response);
}
}
当我们请求成功后,才调用的activity回调方法,所以我们在activity处理的时候必定是成功后的事件。
接下来在我们的请求里做对应的操作:
public class DynamicAddPraiseRequest extends BaseHttpRequestClient {
public long userid;
public long dynamicid;
private MomentsInfo mInfo;
public DynamicAddPraiseRequest(MomentsInfo info) {
mInfo = info;
}
public MomentsInfo getInfo() {
return mInfo;
}
public void setInfo(MomentsInfo info) {
mInfo = info;
}
@Override
public String setUrl() {
return new RequestUrlUtils.Builder().setHost(FriendCircleApp.getRootUrl())
.setPath("/dynamic/addpraise/")
.addParam("userid", userid)
.addParam("dynamicid", dynamicid)
.build();
}
@Override
public void parseResponse(BaseResponse response, JSONObject json, int start, boolean hasMore) throws JSONException {
if (response.getStatus()==200){
DynamicControllerEntity<List<UserInfo>> entity=new DynamicControllerEntity();
entity.setMomentsInfo(mInfo);
List<UserInfo> praiseList= JSONUtil.toList(json.optString("data"),new TypeToken<ArrayList<UserInfo>>(){}
.getType
());
entity.setData(praiseList);
response.setData(entity);
}
}
}
其中RequestUrlUtils是我为了方便写url而写的一个工具类,这里就不展示了。
最后,我们补全activity的回调以及BaseItemDelegate的点击事件处理:
activity:
@Override
public void onResultCallBack(BaseResponse response) {
// 通知更新
switch (response.getRequestType()) {
case RequestType.ADD_PRAISE:
DynamicControllerEntity<List<UserInfo>> entity
= (DynamicControllerEntity<List<UserInfo>>) response.getData();
MomentsInfo info = entity.getMomentsInfo();
info.dynamicInfo.praiseState=CommonValue.HAS_PRAISE;
if (info != null) {
if (info.praiseList != null) {
info.praiseList.clear();
info.praiseList.addAll(entity.getData());
}else {
info.praiseList=entity.getData();
}
}
mAdapter.notifyDataSetChanged();
break;
case RequestType.CANCEL_PRAISE:
DynamicControllerEntity<List<UserInfo>> cancelEntity
= (DynamicControllerEntity<List<UserInfo>>) response.getData();
MomentsInfo mInfo = cancelEntity.getMomentsInfo();
mInfo.dynamicInfo.praiseState=CommonValue.NOT_PRAISE;
if (mInfo != null) {
if (mInfo.praiseList != null) {
mInfo.praiseList.clear();
mInfo.praiseList.addAll(cancelEntity.getData());
}else {
mInfo.praiseList=cancelEntity.getData();
}
}
mAdapter.notifyDataSetChanged();
break;
}
}
BaseItemDelegate:
@Override
public void onClick(View v) {
switch (v.getId()) {
// 评论按钮
case R.id.comment_button:
if (mInfo == null) return;
mCommentPopup.setDynamicInfo(mInfo.dynamicInfo);
mCommentPopup.setOnCommentPopupClickListener(new CommentPopup.OnCommentPopupClickListener() {
@Override
public void onLikeClick(View v, DynamicInfo info) {
if (mDynamicController != null) {
switch (info.praiseState) {
case CommonValue.NOT_PRAISE:
mDynamicController.addPraise(LocalHostInfo.INSTANCE.getHostId(), info.dynamicId,
mInfo, RequestType.ADD_PRAISE);
break;
case CommonValue.HAS_PRAISE:
mDynamicController.cancelPraise(LocalHostInfo.INSTANCE.getHostId(), info.dynamicId,
mInfo, RequestType.CANCEL_PRAISE);
break;
default:
break;
}
}
}
@Override
public void onCommentClick(View v, DynamicInfo info) {
}
});
mCommentPopup.showPopupWindow(commentImage);
break;
default:
break;
}
}
至此,我们的controller中间层实现完成,当然,我觉得这样实现并不太好,原因如下:
- 在CallBack中,我们把MomentsInfo给暴露了,如果出现误操作,导致的就是朋友圈内容显示的问题
- 中间层依赖Request,原因在于MomentsInfo的传值方法,通过request传,这并不太好,因为request理论上应该仅仅负责请求和解析,不应该作为信使。
以上两点在以后我希望等我的水平提高后可以解决甚至重构。如果您有好的建议,希望能在评论区留下脚印或者GitHub提交PR。
<h1 id="step4">Step 4:Popup的补充</h1>
popup在上一篇文章中已经是初步实现了,本篇仅仅针对一些内容进行补充:
我们的评论popup有两个功能:
- 点赞
- 评论
也就是说有两个按钮,但我们不应该把事件都放到popup类里面完成,这会导致耦合度问题(事件处理必定需要viewholder里面的数据,如果在popup里面完成,意味着需要跟viewholder相互依赖),因此,我们采取接口,将点击动作抛出去给viewholder自己处理。
首先我们定义一个接口:
public interface OnCommentPopupClickListener {
void onLikeClick(View v, DynamicInfo info);
void onCommentClick(View v, DynamicInfo info);
}
因为点赞和评论都涉及到数据库对动态id的CRUD操作,所以我们直接传入DynamicInfo(DynamicInfo和接口的setter/getter略)。
然后我们在popup里面实现onClickListener:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.item_like:
if (mOnCommentPopupClickListener != null) {
mOnCommentPopupClickListener.onLikeClick(v, mDynamicInfo);
mLikeView.clearAnimation();
mLikeView.startAnimation(mScaleAnimation);
}
break;
case R.id.item_comment:
if (mOnCommentPopupClickListener != null) {
mOnCommentPopupClickListener.onCommentClick(v, mDynamicInfo);
dismiss();
}
break;
}
}
最后外部viewholder实现接口(见Step 3最后)
事件处理解决后,第二个问题,我们可以看到朋友圈点赞的心心是有一个动画效果的,简单的描述就是:心心放大,然后缩小。
要实现这个效果可以说很简单:给两个Animation,在第一个结束的onAnimationEnd调用第二个Animation不就行了么?
是的,这样是非常简单,也十分明了。但,这样做就需要两个Animation对象,对于内存十分看紧的我,决定使用一个对象完成。
那么,要使用一个Animation完成放大后缩小的效果,就不得不提到插值器这个东东了。
插值器简单的说,就是改变动画的不同时间的值,从而改变动画的变化率。
这里推荐一个网站,这个网站可以将公式可视化为插值器曲线:
http://inloop.github.io/interpolator/
那么要实现先放大后缩小,我们的插值器曲线必定是先上升后下降,这时候很容易想到一个初中学过的东西:三角函数
sin函数在一个周期内有两个峰值,±1,而我们取半个周期就可以得到一条先升后降的曲线了。
如果可视化
效果如下图:
不好意思,因为太好玩了,所以多玩了一会。。。。
在代码上,我们只需要继承LinearInterpolator然后重写getInterpolation就可以了。
static class SpringInterPolator extends LinearInterpolator {
public SpringInterPolator() {
}
@Override
public float getInterpolation(float input) {
return (float) Math.sin(input*Math.PI);
}
}
在动画setInterpolator的时候使用SpringInterPolator即可。
剩下的代码也就不贴了。
下一篇我们完成评论区的事件交互。