項目重構的Git地址:https://github.com/razerdp/FriendCirclejava
下集預告:歡樂的票圈重構之旅——RecyclerView的頭尾佈局增長git
在沉寂了五六個月的時間後,終於有空來收拾一下朋友圈項目的殘局了。 此次換了一個服務器,畢竟我們不是寫後端的,當時一時頭腦發熱,開了一個阿里雲,實際上是爲了畢業設計項目着想,後來實在吃不消108軟妹幣每月的負擔和代碼的維護,因而無奈關掉服務器。 而如今,在平衡了一下LearnCloud和Bmob以後,打算採用Bmob做爲咱們項目的後端支持。 因而乎,在改造的過程當中發現我們的朋友圈項目彷佛要大改,改着改着,乾脆咬咬牙,所有推倒歷來算了(寫過的控件除外)。github
因此,羽翼君又能夠來簡書更新一下文章(騙讚了←_←)。編程
廢話很少說了,直接進入主題。後端
此次重構由於將會從ListView換成RecyclerView,因此不少東西都要從新部署,好比上下拉。服務器
由於朋友圈的特殊性,咱們的上下拉須要符合至少兩個條件:微信
對於懶惰的我來講,首當其衝仍是找庫吧。。。。結果找了一下,瞬間想哭了,由於要同時符合上面兩個條件的,彷佛還真的找不到。。。有一兩個比較接近的(好比:IRecyclerView)卻由於各類問題致使不能使用。。。ide
沒辦法,只好強擼了。。 佈局
因而乎,在一次提交中,狂擼Touch事件。。。 commit here動畫
寫着寫着,想到了還得作動畫,還得作返回,還得作各類各樣的事件分發。。。。天吶嚕,我仍是乖乖去上班吧。。。
這時候突然靈機一閃,想起之前擼ListView時不是有個overScroll的嗎,那Rv也應會有的,因而面向谷歌編程的我,雖然找不到比較好的描述,但找到了這麼一個庫: overscroll-decor
初步看了一下代碼,其核心至關於接管了touch事件,經過setTranslationY來進行View的移動的,並且最重要的是,提供的接口有着狀態和偏移量的返回!!!!(拍黑板,這是重點!)
有了這兩個東西,那就能夠嘿嘿嘿了。
首先,咱們肯定一下咱們的控件應該怎麼寫。
在微信朋友圈中,以咱們的目測,至少有三個要求(本項目以iOS的交互爲標準):
因此,我們的佈局確定不能繼承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-