专业IM即时通讯软件开发,值得信赖!

Android手把手朋友圈实战教程(十六)控件篇(评论popup下+交互事件结构)

即时通讯软件开发 云聊IM 806℃

项目地址:https://github.com/razerdp/FriendCircle

再开始之前,羽翼君想说一件事情,目前我还是一个学生,没什么能力买一个牛逼的服务器,仅仅是学生价租的阿里云,但是,因为为了方便,我把服务器的IP都写在了我们的项目里面,结果他喵的昨晚被攻击了!

这个服务器根本没有什么利用价值,即使是用来做肉鸡,根本没法塞牙缝好么。虽然花的钱不多,也仅仅是为了这个项目和我的毕业设计方便而租的服务器。

当然,这也是我的错,我不应该直接把服务器地址贴上的,这也是我的锅,所以在push的时候我把地址换成了我的吐槽。。。

如果您需要测试数据,您可以简信我或者加我的QQ来拿到地址,非常抱歉我这么做。(我很害怕到毕业设计答辩那天来个攻击啊)

在上篇,我们初步完成了评论popup的展示,这一次我们需要补全剩下的交互代码。

首先上预览图吧(为了方便,以后统一在电脑模拟器上录制):

preview

Step 1:困境
在实现之前,不妨看看我们现在遇到的问题:

如下图:

结构图

从图中我们可以看到,我们现在整一个朋友圈的实现方案如下:

  1. Activity作为一个controller,它现在仅仅负责的是拉取数据,并没有其他的工作。
  2. Adapter,在我们将viewholder抽象出来后,adapter看起来仅仅就是将类型跟对应的viewholder匹配起来,并渲染出来。
  3. 而ViewHolder,则是负责将数据展示,同时一切的数据/操作都是在ViewHolder实现的。

那么问题来了,我们的工程进行到这里,我们其实没有做任何的点击/请求(朋友圈列表拉取除外)而如今,我们需要增加交互等方法,按照图中的结构,我们可以有如下的方法(目前我所想到的):

  1. 因为viewholder持有activity的context,我们可以通过activity提供公用方法,然后使用(if context instance of xxx){ (Activity)context.xxxx}来调用activity的方法
  2. EventBus事件通知
  3. 中间类,使用中间类来处理activity与viewholder之间的交互。

显然,方法一过于笨重不便于扩展,方法二虽然挺方便的,但是在onEventMainThread方法里我们需要很多的判断,所以我们使用方法三。

那么这个中间类是干什么的呢?直观的说,就是如下图这样的结构:

中间层

可能看图还是有点不太明白,那我们通过一个例子来解释一下吧:

假如故事发生在一个初创公司,这个公司目前的分层如下:

  1. BOSS(对应Activity)
  2. 技术总监CTO(对应Adapter)
  3. 具体各个技术小组的leader(对应各个ViewHolder)

在开始阶段因为急需要做出产品给投资人看效果,所以并没有招到很多人,因此一直都是Boss下发需求给CTO,然后CTO评估后再下发给技术小组的leader,然后leader完成需求。
(类比于我们撸朋友圈目前进度:先完成界面展示,而不管任何交互)*

在前期,这样做问题不大,OK,这个初创公司顺利的拿下A轮投资,接下来B轮就需要打造产品特点和细节研磨,这时候就会发现,如果还是按照之前的做法(boss->cto->leader->产品生产),效率大大的降低,同时因为leader忙着忙那,同时应对着boss变来变去的需求,在自己负责的区域应对着一波又一波的需求忙得焦头烂额。(viewholder与activity耦合度过高)

于是,他们决定请人。经过简历筛选,笔试,面试后,他们找到了合适的人选,于是接下来的分工就变成了这样:

  1. boss下发需求(此时其实应该是boss跟产品评估,但为了篇幅,先略过产品)(Activity通知adapter更新)
  2. CTO开评估会议并细分/下发任务(Adapter将数据分发到各个viewholder并渲染)
  3. leader收到任务,开内部会议进行分工,而leader则是负责项目结构,项目基础框架的优化等(viewholder绑定数据并展示)
  4. 各个小组成员收到任务,开始投入生产(码代码)(controll处理各种交互,请求等事件)

然后当各个小组成员完成任务,交由给leader,leader进行review后交由测试,测试通过后可以通知可以准备发版。在各个高层使用过初步满意后正式发版。(在我们的代码里,省略那么多步骤,直接通知activity进行更新)

说了那么多,其实就是一句话:controller承担了最繁琐的步骤,做好后通知activity去更新数据。

Step 2:controller的实现

在一大篇无聊的叙述后,我们就谈谈如何实现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 {
...
    void setController(BaseDynamicController controller);
    BaseDynamicController getController();
}

我们在BaseItemView添加controller的setter/getter,然后在BaseItemDelegate进行赋值,最后就到我们的Adapter进行设置:

CircleBaseAdapter.java:

//因为我们当初设计的时候采用的builder模式,所以我们仅仅需要到builder添加一个参数就好了,这里就不贴代码了。
public CircleBaseAdapter(Activity context, Builder 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。

到这里为止,我们的结构大致完成,接下来就是将剩下的代码补全。

Step 3:代码补全

首先我们思考一下,如何更新我们的界面是最好的。目前来说我想到有以下方法:

  1. 当点赞/取消点赞请求成功后,我们调用activity的列表请求将整个朋友圈数据都重新拉一遍。
  2. 当点赞/取消点赞请求成功后,服务器返回当前动态的点赞列表信息,我们获取当前动态的实体类,解析服务器返回信息后进行更新操作。
  3. 当点赞/取消点赞请求成功后,本地进行插入/删除。

上面几个方案中

第一个方案很明显是不适合的,因为每次点赞都需要将整个列表拉一次,解析耗时不说,就流量消耗也是很可观的。遂放弃。

第二个方案目前采用,但也有不完善的地方,这个下文再说。

第三个方案看似不错,但如果遇到下面这种情况就不适应了:你点赞的同时你好友也点赞,但因为没有数据返回,所以你好友的点赞并没有刷出来。

综上所述,目前采取第二个方案。(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> entity=new DynamicControllerEntity();
            entity.setMomentsInfo(mInfo);
            List praiseList= JSONUtil.toList(json.optString("data"),new TypeToken>(){}
                    .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> entity
                    = (DynamicControllerEntity>) 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> cancelEntity
                    = (DynamicControllerEntity>) 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中间层实现完成,当然,我觉得这样实现并不太好,原因如下:

  1. 在CallBack中,我们把MomentsInfo给暴露了,如果出现误操作,导致的就是朋友圈内容显示的问题
  2. 中间层依赖Request,原因在于MomentsInfo的传值方法,通过request传,这并不太好,因为request理论上应该仅仅负责请求和解析,不应该作为信使。

以上两点在以后我希望等我的水平提高后可以解决甚至重构。如果您有好的建议,希望能在评论区留下脚印或者GitHub提交PR。

Step 4:Popup的补充

popup在上一篇文章中已经是初步实现了,本篇仅仅针对一些内容进行补充:

我们的评论popup有两个功能:

  1. 点赞
  2. 评论

也就是说有两个按钮,但我们不应该把事件都放到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,而我们取半个周期就可以得到一条先升后降的曲线了。

如果可视化

效果如下图:

interpolator

不好意思,因为太好玩了,所以多玩了一会。。。。

在代码上,我们只需要继承LinearInterpolator然后重写getInterpolation就可以了。

static class SpringInterPolator extends LinearInterpolator {

    public SpringInterPolator() {
    }

    @Override
    public float getInterpolation(float input) {
        return (float) Math.sin(input*Math.PI);
    }
}

在动画setInterpolator的时候使用SpringInterPolator即可。

剩下的代码也就不贴了。

下一篇我们完成评论区的事件交互。

版权声明:部分文章、图片等内容为用户发布或互联网整理而来,仅供学习参考。如有侵犯您的版权,请联系我们,将立刻删除。
喜欢 (0)
仿微信聊天软件开发
点击这里给我发消息