項(xiàng)目重構(gòu)的Git地址:
https://github.com/razerdp/FriendCircle/tree/main-dev
項(xiàng)目同步更新的文集:
http://www.reibang.com/notebooks/3224048/latest
下集預(yù)告:歡樂的票圈重構(gòu)之旅——RecyclerView的頭尾布局增加
前言
在沉寂了五六個(gè)月的時(shí)間后棍矛,終于有空來收拾一下朋友圈項(xiàng)目的殘局了。
這次換了一個(gè)服務(wù)器阴孟,畢竟咱們不是寫后端的耘眨,當(dāng)時(shí)一時(shí)頭腦發(fā)熱暮屡,開了一個(gè)阿里云,其實(shí)是為了畢業(yè)設(shè)計(jì)項(xiàng)目著想毅桃,后來實(shí)在吃不消108軟妹幣每個(gè)月的負(fù)擔(dān)和代碼的維護(hù)褒纲,于是無奈關(guān)掉服務(wù)器。
而現(xiàn)在钥飞,在平衡了一下LearnCloud和Bmob之后莺掠,打算采用Bmob作為我們項(xiàng)目的后端支持。
于是乎读宙,在改造的過程中發(fā)現(xiàn)咱們的朋友圈項(xiàng)目似乎要大改彻秆,改著改著,干脆咬咬牙结闸,全部推倒從來算了(寫過的控件除外)唇兑。
所以,羽翼君又可以來簡書更新一下文章(騙贊了←_←)桦锄。
Rv的上下拉
廢話不多說了扎附,直接進(jìn)入主題。
這次重構(gòu)因?yàn)閷腖istView換成RecyclerView结耀,所以很多東西都要重新部署留夜,比如上下拉。
因?yàn)榕笥讶Φ奶厥庑酝继穑覀兊纳舷吕枰现辽賰蓚€(gè)條件:
- 下拉刷新可以獲取到偏移量(用來聯(lián)動logo)
- 下拉刷新時(shí)碍粥,可以隱藏刷新頭部,而只展示我們的logo動畫
對于懶惰的我來說黑毅,首當(dāng)其沖還是找?guī)彀山滥Α!?笫荨U砻妗=Y(jié)果找了一下,瞬間想哭了匪凡,因?yàn)橐瑫r(shí)符合上面兩個(gè)條件的膊畴,似乎還真的找不到。病游。唇跨。有一兩個(gè)比較接近的(比如:IRecyclerView)卻因?yàn)楦鞣N問題導(dǎo)致不能使用。衬衬。买猖。
沒辦法,只好強(qiáng)擼了滋尉。玉控。
于是乎,在一次提交中狮惜,狂擼Touch事件高诺。碌识。。
commit here
寫著寫著虱而,想到了還得做動畫筏餐,還得做返回,還得做各種各樣的事件分發(fā)牡拇。魁瞪。。惠呼。天吶嚕导俘,我還是乖乖去上班吧。剔蹋。旅薄。
這時(shí)候忽然靈機(jī)一閃,想起以前擼ListView時(shí)不是有個(gè)overScroll的嗎滩租,那Rv也應(yīng)會有的赋秀,于是面向谷歌編程的我,雖然找不到比較好的描述律想,但找到了這么一個(gè)庫:
overscroll-decor
初步看了一下代碼猎莲,其核心相當(dāng)于接管了touch事件,通過setTranslationY來進(jìn)行View的移動的技即,而且最重要的是著洼,提供的接口有著狀態(tài)和偏移量的返回!6稹I眢浴!(拍黑板葵陵,這是重點(diǎn)液荸!)
有了這兩個(gè)東西,那就可以嘿嘿嘿了脱篙。
控件的布局
首先娇钱,我們確定一下我們的控件應(yīng)該怎么寫。
在微信朋友圈中绊困,以我們的目測文搂,至少有三個(gè)要求(本項(xiàng)目以iOS的交互為標(biāo)準(zhǔn)):
- (1) logo要隨著下拉的動作同時(shí)下拉
- (2) RecyclerView拉下來之后,要露出后面的背景
- (3) 咱們的logo是跟RecyclerView同級的
所以秤朗,咱們的布局肯定不能繼承RecyclerView然后干煤蹭,而是一個(gè)ViewGroup,這次我選擇了FrameLayout硝皂。
所以咱們的初始化這么寫:
//構(gòu)造器什么的常挚,忽略啦~都指向于這里
private void init(Context context) {
//漸變背景(黑色的背景在上半部分,下半部分是白色的)
GradientDrawable background = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0xff323232, 0xff323232, 0xffffffff, 0xffffffff});
setBackground(background);
//rv初始化
if (recyclerView == null) {
recyclerView = new RecyclerView(context);
recyclerView.setBackgroundColor(Color.WHITE);
recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
}
//logo初始化
if (refreshIcon == null) {
refreshIcon = new ImageView(context);
refreshIcon.setBackgroundColor(Color.TRANSPARENT);
refreshIcon.setImageResource(R.drawable.rotate_icon);
}
FrameLayout.LayoutParams iconParam = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
iconParam.leftMargin = UIHelper.dipToPx(12);
//add
addView(recyclerView, RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.MATCH_PARENT);
addView(refreshIcon, iconParam);
//觸發(fā)刷新的警戒線
refreshPosition = UIHelper.dipToPx(90);
//logo的觀察類
iconObserver = new InnerRefreshIconObserver(refreshIcon, refreshPosition);
}
接下來就是咱們的下拉刷新了。前面說過怨酝,咱么用的是overscroll那個(gè)庫傀缩,我們針對的是偏移量,所以我們所有的工作都依賴于這個(gè)偏移:
private void initOverScroll() {
IOverScrollDecor decor = new VerticalOverScrollBounceEffectDecorator(new RecyclerViewOverScrollDecorAdapter(recyclerView), 2f, 1f, 2f);
decor.setOverScrollUpdateListener(new IOverScrollUpdateListener() {
@Override
public void onOverScrollUpdate(IOverScrollDecor decor, int state, float offset) {
if (offset > 0) {
//正在刷新就不鳥它
if (currentStatus == REFRESHING) return;
//更新logo的位置
iconObserver.catchPullEvent(offset);
if (offset >= refreshPosition && state == STATE_BOUNCE_BACK) {
//state變成返回時(shí)农猬,意味著已經(jīng)松手了赡艰,則進(jìn)行刷新邏輯
if (currentStatus != REFRESHING) {
setCurrentStatus(REFRESHING);
if (onRefreshListener != null) {
Log.i(TAG, "refresh");
onRefreshListener.onRefresh();
}
iconObserver.catchRefreshEvent();
}
}
} else if (offset < 0) {
//底部的overscroll
}
}
});
}
代碼不多,因?yàn)槎嗟臇|西都在庫里面干完了斤葱。慷垮。。
在調(diào)用了setAdapter之后揍堕,我們執(zhí)行這個(gè)初始化方法料身,從回調(diào)的接口處,不難看到offset的回調(diào)有兩種衩茸,分別是大于0和小于0芹血,其中大于0是從頂部下拉(下拉刷新),而小于0則是從底部上拉(上拉加載)楞慈。
但是幔烛,有一個(gè)問題是,我們沒有辦法知道松手的觸發(fā)囊蓝,也就是相當(dāng)于touch的up事件饿悬。不過幸好,接口同時(shí)還返回了狀態(tài)聚霜,當(dāng)狀態(tài)發(fā)生改變的時(shí)候狡恬,就肯定是手勢發(fā)生了變化,通過狀態(tài)俯萎,我們就相當(dāng)于捕捉到了up事件傲宜。所以就有了以上的代碼。
因?yàn)榕笥讶Σ⒉恍枰侠虞d夫啊,而是滑動到底部自動加載更多函卒,所以這offset<0的地方我就沒有做任何邏輯了,如果有需求的話,也是可以做到上拉加載更多的报嵌。
做完上下拉的邏輯之后虱咧,接下來就是logo的聯(lián)動。
從代碼上來看锚国,我把所有的邏輯都封到了iconObserver里面了(其實(shí)我覺得起名叫iconHelper可能更好腕巡,但就是覺得Observer高大上一點(diǎn)←_←)。
在observer里面血筑,我們主要做的東西都是跟UI有關(guān)的绘沉。代碼比較簡單,所有就把解釋寫到代碼里面了
/**
* 刷新Icon的動作觀察者
*/
private static class InnerRefreshIconObserver {
private ImageView refreshIcon;
private final int refreshPosition;
private float lastOffset = 0.0f;
private RotateAnimation rotateAnimation;
private ValueAnimator mValueAnimator;
public InnerRefreshIconObserver(ImageView refreshIcon, int refreshPosition) {
this.refreshIcon = refreshIcon;
this.refreshPosition = refreshPosition;
rotateAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setDuration(600);
rotateAnimation.setInterpolator(new LinearInterpolator());
rotateAnimation.setRepeatCount(Animation.INFINITE);
}
public void catchPullEvent(float offset) {
if (checkHacIcon()) {
refreshIcon.setRotation(offset * 2);
if (offset >= refreshPosition) {
offset = refreshPosition;
}
int resultOffset = (int) (offset - lastOffset);
refreshIcon.offsetTopAndBottom(resultOffset);
Log.d(TAG, "pull >> " + offset + " resultOffset >>> " + resultOffset);
adjustRefreshIconPosition();
lastOffset = offset;
}
}
/**
* 調(diào)整icon的位置界限
*/
private void adjustRefreshIconPosition() {
if (refreshIcon.getTop() < 0) {
refreshIcon.offsetTopAndBottom(Math.abs(refreshIcon.getTop()));
} else if (refreshIcon.getTop() > refreshPosition) {
refreshIcon.offsetTopAndBottom(-(refreshIcon.getTop() - refreshPosition));
}
}
public void catchRefreshEvent() {
if (checkHacIcon()) {
refreshIcon.clearAnimation();
refreshIcon.startAnimation(rotateAnimation);
}
}
public void catchResetEvent() {
refreshIcon.clearAnimation();
if (mValueAnimator == null) {
mValueAnimator = ValueAnimator.ofFloat(refreshPosition, 0);
mValueAnimator.setInterpolator(new DecelerateInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float result = (float) animation.getAnimatedValue();
catchPullEvent(result);
}
});
mValueAnimator.setDuration(300);
}
mValueAnimator.start();
}
private boolean checkHacIcon() {
return refreshIcon != null;
}
}
最后是demo動圖:
本篇比較簡單豺总,算是一個(gè)開始吧车伞,接下來的重構(gòu)咱么就愉快地進(jìn)行吧-V-