專案重構的Git地址:https://github.com/razerdp/FriendCircle
下集預告:歡樂的票圈重構之旅——RecyclerView的頭尾佈局增加
前言
在沉寂了五六個月的時間後,終於有空來收拾一下朋友圈專案的殘局了。 這次換了一個伺服器,畢竟我們們不是寫後端的,當時一時頭腦發熱,開了一個阿里雲,其實是為了畢業設計專案著想,後來實在吃不消108軟妹幣每個月的負擔和程式碼的維護,於是無奈關掉伺服器。 而現在,在平衡了一下LearnCloud和Bmob之後,打算採用Bmob作為我們專案的後端支援。 於是乎,在改造的過程中發現我們們的朋友圈專案似乎要大改,改著改著,乾脆咬咬牙,全部推倒從來算了(寫過的控制元件除外)。
所以,羽翼君又可以來簡書更新一下文章(騙讚了←_←)。
Rv的上下拉
廢話不多說了,直接進入主題。
這次重構因為將會從ListView換成RecyclerView,所以很多東西都要重新部署,比如上下拉。
因為朋友圈的特殊性,我們的上下拉需要符合至少兩個條件:
- 下拉重新整理可以獲取到偏移量(用來聯動logo)
- 下拉重新整理時,可以隱藏重新整理頭部,而只展示我們的logo動畫
對於懶惰的我來說,首當其衝還是找庫吧。。。。結果找了一下,瞬間想哭了,因為要同時符合上面兩個條件的,似乎還真的找不到。。。有一兩個比較接近的(比如:IRecyclerView)卻因為各種問題導致不能使用。。。
沒辦法,只好強擼了。。
於是乎,在一次提交中,狂擼Touch事件。。。 commit here
寫著寫著,想到了還得做動畫,還得做返回,還得做各種各樣的事件分發。。。。天吶嚕,我還是乖乖去上班吧。。。
這時候忽然靈機一閃,想起以前擼ListView時不是有個overScroll的嗎,那Rv也應會有的,於是面向谷歌程式設計的我,雖然找不到比較好的描述,但找到了這麼一個庫: overscroll-decor
初步看了一下程式碼,其核心相當於接管了touch事件,通過setTranslationY來進行View的移動的,而且最重要的是,提供的介面有著狀態和偏移量的返回!!!!(拍黑板,這是重點!)
有了這兩個東西,那就可以嘿嘿嘿了。
控制元件的佈局
首先,我們確定一下我們的控制元件應該怎麼寫。
在微信朋友圈中,以我們的目測,至少有三個要求(本專案以iOS的互動為標準):
- (1) logo要隨著下拉的動作同時下拉
- (2) RecyclerView拉下來之後,要露出後面的背景
- (3) 我們們的logo是跟RecyclerView同級的
所以,我們們的佈局肯定不能繼承RecyclerView然後幹,而是一個ViewGroup,這次我選擇了FrameLayout。
所以我們們的初始化這麼寫:
//構造器什麼的,忽略啦~都指向於這裡
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);
//觸發重新整理的警戒線
refreshPosition = UIHelper.dipToPx(90);
//logo的觀察類
iconObserver = new InnerRefreshIconObserver(refreshIcon, refreshPosition);
}
複製程式碼
接下來就是我們們的下拉重新整理了。前面說過,我們麼用的是overscroll那個庫,我們針對的是偏移量,所以我們所有的工作都依賴於這個偏移:
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變成返回時,意味著已經鬆手了,則進行重新整理邏輯
if (currentStatus != REFRESHING) {
setCurrentStatus(REFRESHING);
if (onRefreshListener != null) {
Log.i(TAG, "refresh");
onRefreshListener.onRefresh();
}
iconObserver.catchRefreshEvent();
}
}
} else if (offset < 0) {
//底部的overscroll
}
}
});
}
複製程式碼
程式碼不多,因為多的東西都在庫裡面幹完了。。。
在呼叫了setAdapter之後,我們執行這個初始化方法,從回撥的介面處,不難看到offset的回撥有兩種,分別是大於0和小於0,其中大於0是從頂部下拉(下拉重新整理),而小於0則是從底部上拉(上拉載入)。
但是,有一個問題是,我們沒有辦法知道鬆手的觸發,也就是相當於touch的up事件。不過幸好,介面同時還返回了狀態,當狀態發生改變的時候,就肯定是手勢發生了變化,通過狀態,我們就相當於捕捉到了up事件。所以就有了以上的程式碼。
因為朋友圈並不需要上拉載入,而是滑動到底部自動載入更多,所以這offset<0的地方我就沒有做任何邏輯了,如果有需求的話,也是可以做到上拉載入更多的。
做完上下拉的邏輯之後,接下來就是logo的聯動。
從程式碼上來看,我把所有的邏輯都封到了iconObserver裡面了(其實我覺得起名叫iconHelper可能更好,但就是覺得Observer高大上一點←_←)。
在observer裡面,我們主要做的東西都是跟UI有關的。程式碼比較簡單,所有就把解釋寫到程式碼裡面了
/**
* 重新整理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;
}
}
/**
* 調整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動圖:
本篇比較簡單,算是一個開始吧,接下來的重構我們麼就愉快地進行吧-V-