項(xiàng)目地址:https://github.com/razerdp/FriendCircle
一起擼個(gè)朋友圈吧這是本文所處文集,所有更新都會(huì)在這個(gè)文集里面哦,歡迎關(guān)注
上篇鏈接:http://www.itdecent.cn/p/15a9fe8f917f
下篇鏈接:http://www.itdecent.cn/p/58894dfb3f09
本篇圖文較多,流量黨請(qǐng)慎重
再開(kāi)始之前,羽翼君想說(shuō)一件事情,目前我還是一個(gè)學(xué)生,沒(méi)什么能力買(mǎi)一個(gè)牛逼的服務(wù)器,僅僅是學(xué)生價(jià)租的阿里云,但是,因?yàn)闉榱朔奖?,我把服?wù)器的IP都寫(xiě)在了我們的項(xiàng)目里面,結(jié)果他喵的昨晚被攻擊了!
這個(gè)服務(wù)器根本沒(méi)有什么利用價(jià)值,即使是用來(lái)做肉雞,根本沒(méi)法塞牙縫好么。雖然花的錢(qián)不多,也僅僅是為了這個(gè)項(xiàng)目和我的畢業(yè)設(shè)計(jì)方便而租的服務(wù)器。
當(dāng)然,這也是我的錯(cuò),我不應(yīng)該直接把服務(wù)器地址貼上的,這也是我的鍋,所以在push的時(shí)候我把地址換成了我的吐槽。。。
如果您需要測(cè)試數(shù)據(jù),您可以簡(jiǎn)信我或者加我的QQ來(lái)拿到地址,非常抱歉我這么做。(我很害怕到畢業(yè)設(shè)計(jì)答辯那天來(lái)個(gè)攻擊啊)
【END】
在上篇,我們初步完成了評(píng)論popup的展示,這一次我們需要補(bǔ)全剩下的交互代碼。
首先上預(yù)覽圖吧(為了方便,以后統(tǒng)一在電腦模擬器上錄制):

<h1 id="step1">Step 1:困境</h1>
在實(shí)現(xiàn)之前,不妨看看我們現(xiàn)在遇到的問(wèn)題:
如下圖:

從圖中我們可以看到,我們現(xiàn)在整一個(gè)朋友圈的實(shí)現(xiàn)方案如下:
- Activity作為一個(gè)controller,它現(xiàn)在僅僅負(fù)責(zé)的是拉取數(shù)據(jù),并沒(méi)有其他的工作。
- Adapter,在我們將viewholder抽象出來(lái)后,adapter看起來(lái)僅僅就是將類(lèi)型跟對(duì)應(yīng)的viewholder匹配起來(lái),并渲染出來(lái)。
- 而ViewHolder,則是負(fù)責(zé)將數(shù)據(jù)展示,同時(shí)一切的數(shù)據(jù)/操作都是在ViewHolder實(shí)現(xiàn)的。
那么問(wèn)題來(lái)了,我們的工程進(jìn)行到這里,我們其實(shí)沒(méi)有做任何的點(diǎn)擊/請(qǐng)求(朋友圈列表拉取除外)而如今,我們需要增加交互等方法,按照?qǐng)D中的結(jié)構(gòu),我們可以有如下的方法(目前我所想到的):
- 因?yàn)関iewholder持有activity的context,我們可以通過(guò)activity提供公用方法,然后使用(if context instance of xxx){ (Activity)context.xxxx}來(lái)調(diào)用activity的方法
- EventBus事件通知
- 中間類(lèi),使用中間類(lèi)來(lái)處理activity與viewholder之間的交互。
顯然,方法一過(guò)于笨重不便于擴(kuò)展,方法二雖然挺方便的,但是在onEventMainThread方法里我們需要很多的判斷,所以我們使用方法三。
那么這個(gè)中間類(lèi)是干什么的呢?直觀(guān)的說(shuō),就是如下圖這樣的結(jié)構(gòu):

可能看圖還是有點(diǎn)不太明白,那我們通過(guò)一個(gè)例子來(lái)解釋一下吧:
假如故事發(fā)生在一個(gè)初創(chuàng)公司,這個(gè)公司目前的分層如下:
- BOSS(對(duì)應(yīng)
Activity) - 技術(shù)總監(jiān)CTO(對(duì)應(yīng)
Adapter) - 具體各個(gè)技術(shù)小組的leader(對(duì)應(yīng)各個(gè)
ViewHolder)
在開(kāi)始階段因?yàn)榧毙枰龀霎a(chǎn)品給投資人看效果,所以并沒(méi)有招到很多人,因此一直都是Boss下發(fā)需求給CTO,然后CTO評(píng)估后再下發(fā)給技術(shù)小組的leader,然后leader完成需求。
(類(lèi)比于我們擼朋友圈目前進(jìn)度:先完成界面展示,而不管任何交互)*
在前期,這樣做問(wèn)題不大,OK,這個(gè)初創(chuàng)公司順利的拿下A輪投資,接下來(lái)B輪就需要打造產(chǎn)品特點(diǎn)和細(xì)節(jié)研磨,這時(shí)候就會(huì)發(fā)現(xiàn),如果還是按照之前的做法(boss->cto->leader->產(chǎn)品生產(chǎn)),效率大大的降低,同時(shí)因?yàn)閘eader忙著忙那,同時(shí)應(yīng)對(duì)著boss變來(lái)變?nèi)サ男枨?,在自己?fù)責(zé)的區(qū)域應(yīng)對(duì)著一波又一波的需求忙得焦頭爛額。(viewholder與activity耦合度過(guò)高)
于是,他們決定請(qǐng)人。經(jīng)過(guò)簡(jiǎn)歷篩選,筆試,面試后,他們找到了合適的人選,于是接下來(lái)的分工就變成了這樣:
- boss下發(fā)需求(此時(shí)其實(shí)應(yīng)該是boss跟產(chǎn)品評(píng)估,但為了篇幅,先略過(guò)產(chǎn)品)(Activity通知adapter更新)
- CTO開(kāi)評(píng)估會(huì)議并細(xì)分/下發(fā)任務(wù)(Adapter將數(shù)據(jù)分發(fā)到各個(gè)viewholder并渲染)
- leader收到任務(wù),開(kāi)內(nèi)部會(huì)議進(jìn)行分工,而leader則是負(fù)責(zé)項(xiàng)目結(jié)構(gòu),項(xiàng)目基礎(chǔ)框架的優(yōu)化等(viewholder綁定數(shù)據(jù)并展示)
- 各個(gè)小組成員收到任務(wù),開(kāi)始投入生產(chǎn)(碼代碼)(controll處理各種交互,請(qǐng)求等事件)
然后當(dāng)各個(gè)小組成員完成任務(wù),交由給leader,leader進(jìn)行review后交由測(cè)試,測(cè)試通過(guò)后可以通知可以準(zhǔn)備發(fā)版。在各個(gè)高層使用過(guò)初步滿(mǎn)意后正式發(fā)版。(在我們的代碼里,省略那么多步驟,直接通知activity進(jìn)行更新)
說(shuō)了那么多,其實(shí)就是一句話(huà):controller承擔(dān)了最繁瑣的步驟,做好后通知activity去更新數(shù)據(jù)。
<h1 id="step2">Step 2:controller的實(shí)現(xiàn)</h1>
在一大篇無(wú)聊的敘述后,我們就談?wù)勅绾螌?shí)現(xiàn)controller。談起controller,就不得不想到MVC,進(jìn)而想到MVP。我們這里并非實(shí)現(xiàn)MVP,但總的來(lái)說(shuō),有點(diǎn)形似而神不似吧。
首先既然要做解耦,那就必須涉及到抽象,而抽象,就我經(jīng)驗(yàn)來(lái)說(shuō),接口化應(yīng)該是最好的。
所以我們先抽象出一個(gè)BaseController(注:此接口不遵循單一職責(zé)原則):
/**
* Created by 大燈泡 on 2016/3/9.
* 控制器接口化
*/
public interface BaseDynamicController {
// 點(diǎn)贊
void addPraise(long userid, long dynamicid, MomentsInfo info, @RequestType.DynamicRequestType int requesttype);
// 取消點(diǎn)贊
void cancelPraise(long userid, long dynamicid, MomentsInfo info, @RequestType.DynamicRequestType int requesttype);
}
我們需要的操作都將會(huì)在接口里限定。
關(guān)于@RequestType.DynamicRequestType,一般而言,在需要傳入一定范圍內(nèi)的值時(shí),我們應(yīng)該使用注解限定,這樣可以降低誤操作。另外RequestType用于區(qū)分同一個(gè)類(lèi)下多個(gè)請(qǐng)求。
所以這里限定傳入的值必須是DynamicRequestType所支持的值:
public class RequestType {
@Retention(RetentionPolicy.SOURCE)
@IntDef({ADD_PRAISE,CANCEL_PRAISE})
public @interface DynamicRequestType{}
// 點(diǎn)贊
public static final int ADD_PRAISE=0x10;
public static final int CANCEL_PRAISE=0x11;
}
目前我們只做了點(diǎn)贊和取消點(diǎn)贊,所以暫時(shí)只需要這兩個(gè)類(lèi)型。
接口寫(xiě)完后,我們接下來(lái)需要實(shí)現(xiàn)這個(gè)接口,定義一個(gè)類(lèi)DynamicController并實(shí)現(xiàn)請(qǐng)求回調(diào)接口以及剛剛我們定義的controller接口:
/**
* Created by 大燈泡 on 2016/3/8.
* 事件控制器
* 本控制器用于BaseItemDelegate的事件處理
* 事件處理完成通過(guò)callback回調(diào)給activity,避免BaseItem與activity耦合度過(guò)高
*/
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進(jìn)行數(shù)據(jù)更新,所以我們需要定一個(gè)CallBack,讓activity實(shí)現(xiàn)這個(gè)接口。同時(shí)為了緊張的內(nèi)存,我們還需要定義一個(gè)destroy方法,及時(shí)的進(jìn)行對(duì)象置空。
接下來(lái)改造一下我們的BaseItemDelegate,因?yàn)槲覀儺?dāng)初設(shè)計(jì)的時(shí)候是采取接口的形式,所以我們實(shí)質(zhì)上是改造BaseItemView
public interface BaseItemView<T> {
...
void setController(BaseDynamicController controller);
BaseDynamicController getController();
}
我們?cè)贐aseItemView添加controller的setter/getter,然后在BaseItemDelegate進(jìn)行賦值,最后就到我們的Adapter進(jìn)行設(shè)置:
CircleBaseAdapter.java:
//因?yàn)槲覀儺?dāng)初設(shè)計(jì)的時(shí)候采用的builder模式,所以我們僅僅需要到builder添加一個(gè)參數(shù)就好了,這里就不貼代碼了。
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;
}
因?yàn)橹貜?fù)的代碼在之前的簡(jiǎn)書(shū)都有記錄,所以這里就略過(guò)了,如果您看的云里霧里,可以在這篇文章看到所有的解析。
在viewholder和controller完成后,我們最后需要在activity將controller給new出來(lái),然后添加到builder里面,使adapter,activity共同持有一個(gè)對(duì)象。
public class FriendCircleDemoActivity extends FriendCircleBaseActivity implements DynamicController.CallBack {
private FriendCircleRequest mCircleRequest;
private DynamicController mDynamicController;
// 方案二,預(yù)留
/* @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的代碼略過(guò),詳情可以看GitHub。
到這里為止,我們的結(jié)構(gòu)大致完成,接下來(lái)就是將剩下的代碼補(bǔ)全。
<h1 id="step3">Step 3:代碼補(bǔ)全</h1>
首先我們思考一下,如何更新我們的界面是最好的。目前來(lái)說(shuō)我想到有以下方法:
- 當(dāng)點(diǎn)贊/取消點(diǎn)贊請(qǐng)求成功后,我們調(diào)用activity的列表請(qǐng)求將整個(gè)朋友圈數(shù)據(jù)都重新拉一遍。
- 當(dāng)點(diǎn)贊/取消點(diǎn)贊請(qǐng)求成功后,服務(wù)器返回當(dāng)前動(dòng)態(tài)的點(diǎn)贊列表信息,我們獲取當(dāng)前動(dòng)態(tài)的實(shí)體類(lèi),解析服務(wù)器返回信息后進(jìn)行更新操作。
- 當(dāng)點(diǎn)贊/取消點(diǎn)贊請(qǐng)求成功后,本地進(jìn)行插入/刪除。
上面幾個(gè)方案中
第一個(gè)方案很明顯是不適合的,因?yàn)槊看吸c(diǎn)贊都需要將整個(gè)列表拉一次,解析耗時(shí)不說(shuō),就流量消耗也是很可觀(guān)的。遂放棄。
第二個(gè)方案目前采用,但也有不完善的地方,這個(gè)下文再說(shuō)。
第三個(gè)方案看似不錯(cuò),但如果遇到下面這種情況就不適應(yīng)了:你點(diǎn)贊的同時(shí)你好友也點(diǎn)贊,但因?yàn)闆](méi)有數(shù)據(jù)返回,所以你好友的點(diǎn)贊并沒(méi)有刷出來(lái)。
綜上所述,目前采取第二個(gè)方案。(ps:第二個(gè)方案目前我的實(shí)現(xiàn)并不好,但暫時(shí)沒(méi)有想到更好的方案,如果您有好的建議,在下衷心希望可以留下您的評(píng)論)
首先回到我們的controller中,在CallBack里我們傳遞的參數(shù)是BaseResponse,但BaseResponse當(dāng)初我們?cè)O(shè)計(jì)的時(shí)候,其接受的數(shù)據(jù)如下:
public class BaseResponse {
//請(qǐng)求碼
private int status;
//錯(cuò)誤碼
private int errorCode;
//請(qǐng)求類(lèi)型,用于單activity多個(gè)請(qǐng)求的區(qū)分
private int requestType;
//請(qǐng)求回來(lái)的JSON字符串
private String jsonStr;
//錯(cuò)誤信息
private String errorMsg;
//待用,可以存放解析后的JSON Array
private ArrayList<Object> datas=new ArrayList<>();
//存放解析后的數(shù)據(jù)
private Object data;
//是否展示dialog
private boolean showDialog;
private int start;
private boolean hasMore;
...
}
可以看到,我們存放數(shù)據(jù)的地方僅僅只有一個(gè)Object,但我們需要拿到一個(gè)動(dòng)態(tài)實(shí)體和點(diǎn)贊后服務(wù)器返回的數(shù)據(jù)解析。當(dāng)然,我們可以選擇在BaseResponse再添加一個(gè)對(duì)象來(lái)存放,但如果這樣做,就會(huì)導(dǎo)致以后這個(gè)類(lèi)也許會(huì)越來(lái)越臃腫。而這并不是我所希望看到的。
所以,我們需要開(kāi)辟一個(gè)新的用于controller的實(shí)體:
/**
* Created by 大燈泡 on 2016/3/10.
* 控制器實(shí)體類(lèi)
*/
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;
}
}
做好這個(gè)之后,我們補(bǔ)全controller的內(nèi)容:
/**
* Created by 大燈泡 on 2016/3/8.
* 事件控制器
* 本控制器用于BaseItemDelegate的事件處理
* 事件處理完成通過(guò)callback回調(diào)給activity,避免BaseItem與activity耦合度過(guò)高
*/
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);
}
}
當(dāng)我們請(qǐng)求成功后,才調(diào)用的activity回調(diào)方法,所以我們?cè)赼ctivity處理的時(shí)候必定是成功后的事件。
接下來(lái)在我們的請(qǐng)求里做對(duì)應(yīng)的操作:
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是我為了方便寫(xiě)url而寫(xiě)的一個(gè)工具類(lèi),這里就不展示了。
最后,我們補(bǔ)全activity的回調(diào)以及BaseItemDelegate的點(diǎn)擊事件處理:
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()) {
// 評(píng)論按鈕
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中間層實(shí)現(xiàn)完成,當(dāng)然,我覺(jué)得這樣實(shí)現(xiàn)并不太好,原因如下:
- 在CallBack中,我們把MomentsInfo給暴露了,如果出現(xiàn)誤操作,導(dǎo)致的就是朋友圈內(nèi)容顯示的問(wèn)題
- 中間層依賴(lài)Request,原因在于MomentsInfo的傳值方法,通過(guò)request傳,這并不太好,因?yàn)閞equest理論上應(yīng)該僅僅負(fù)責(zé)請(qǐng)求和解析,不應(yīng)該作為信使。
以上兩點(diǎn)在以后我希望等我的水平提高后可以解決甚至重構(gòu)。如果您有好的建議,希望能在評(píng)論區(qū)留下腳印或者GitHub提交PR。
<h1 id="step4">Step 4:Popup的補(bǔ)充</h1>
popup在上一篇文章中已經(jīng)是初步實(shí)現(xiàn)了,本篇僅僅針對(duì)一些內(nèi)容進(jìn)行補(bǔ)充:
我們的評(píng)論popup有兩個(gè)功能:
- 點(diǎn)贊
- 評(píng)論
也就是說(shuō)有兩個(gè)按鈕,但我們不應(yīng)該把事件都放到popup類(lèi)里面完成,這會(huì)導(dǎo)致耦合度問(wèn)題(事件處理必定需要viewholder里面的數(shù)據(jù),如果在popup里面完成,意味著需要跟viewholder相互依賴(lài)),因此,我們采取接口,將點(diǎn)擊動(dòng)作拋出去給viewholder自己處理。
首先我們定義一個(gè)接口:
public interface OnCommentPopupClickListener {
void onLikeClick(View v, DynamicInfo info);
void onCommentClick(View v, DynamicInfo info);
}
因?yàn)辄c(diǎn)贊和評(píng)論都涉及到數(shù)據(jù)庫(kù)對(duì)動(dòng)態(tài)id的CRUD操作,所以我們直接傳入DynamicInfo(DynamicInfo和接口的setter/getter略)。
然后我們?cè)趐opup里面實(shí)現(xiàn)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實(shí)現(xiàn)接口(見(jiàn)Step 3最后)
事件處理解決后,第二個(gè)問(wèn)題,我們可以看到朋友圈點(diǎn)贊的心心是有一個(gè)動(dòng)畫(huà)效果的,簡(jiǎn)單的描述就是:心心放大,然后縮小。
要實(shí)現(xiàn)這個(gè)效果可以說(shuō)很簡(jiǎn)單:給兩個(gè)Animation,在第一個(gè)結(jié)束的onAnimationEnd調(diào)用第二個(gè)Animation不就行了么?
是的,這樣是非常簡(jiǎn)單,也十分明了。但,這樣做就需要兩個(gè)Animation對(duì)象,對(duì)于內(nèi)存十分看緊的我,決定使用一個(gè)對(duì)象完成。
那么,要使用一個(gè)Animation完成放大后縮小的效果,就不得不提到插值器這個(gè)東東了。
插值器簡(jiǎn)單的說(shuō),就是改變動(dòng)畫(huà)的不同時(shí)間的值,從而改變動(dòng)畫(huà)的變化率。
這里推薦一個(gè)網(wǎng)站,這個(gè)網(wǎng)站可以將公式可視化為插值器曲線(xiàn):
http://inloop.github.io/interpolator/
那么要實(shí)現(xiàn)先放大后縮小,我們的插值器曲線(xiàn)必定是先上升后下降,這時(shí)候很容易想到一個(gè)初中學(xué)過(guò)的東西:三角函數(shù)
sin函數(shù)在一個(gè)周期內(nèi)有兩個(gè)峰值,±1,而我們?nèi)“雮€(gè)周期就可以得到一條先升后降的曲線(xiàn)了。
如果可視化
效果如下圖:

不好意思,因?yàn)樘猛媪?,所以多玩了一?huì)。。。。
在代碼上,我們只需要繼承LinearInterpolator然后重寫(xiě)getInterpolation就可以了。
static class SpringInterPolator extends LinearInterpolator {
public SpringInterPolator() {
}
@Override
public float getInterpolation(float input) {
return (float) Math.sin(input*Math.PI);
}
}
在動(dòng)畫(huà)setInterpolator的時(shí)候使用SpringInterPolator即可。
剩下的代碼也就不貼了。
下一篇我們完成評(píng)論區(qū)的事件交互。