前面第一章我們講了刷新控件的初步實(shí)現(xiàn),基本上已經(jīng)處理了它的手勢,狀態(tài)切換甚至還有UI交互效果(松手的時候自己滾動),那么我們還剩下什么呢仑荐?
- 現(xiàn)在我們的refreshView和loadView都還只是兩個啥都沒有的view,太丑了纵东,我們其實(shí)是想要動畫效果粘招,所以我們需要加動畫
- 現(xiàn)在刷新控件就只是能被拖動,實(shí)際上我們能在項(xiàng)目里面用嗎偎球?不能洒扎,因?yàn)槲覀儧]有掛監(jiān)聽,當(dāng)前狀態(tài)變成刷新或者加載了不會去通知我們的業(yè)務(wù)模塊
接下來我們先把動畫加上去
說到動畫衰絮,我們可以直接用ANIMATION逊笆,也可以有諸如寫成XML的方式等等。這里偷一下懶(先搞一個動畫放上去再說)岂傲,我們直接扒其他項(xiàng)目已經(jīng)寫好的動畫MaterialDrawable
。
從表現(xiàn)上看子檀,它就是安卓5.0風(fēng)格的進(jìn)度條镊掖,差不多是這個樣子
從實(shí)現(xiàn)上來說乃戈,它其實(shí)是一個Drawable
,就是說其實(shí)是通過不停的draw
來達(dá)到動畫的效果的亩进,這一點(diǎn)跟view
的draw
是一個道理症虑。正因?yàn)樗蕾?code>draw,所以后面我們會看到归薛,在手勢拖動的過程中谍憔,為了實(shí)時的讓mRefreshDrawable
和mLoadDrawable
發(fā)生視覺上的變化,我們不得不在處理拖動的時候調(diào)用invalidate
來刷新整個控件主籍。然后這個Drawable
就是在每一幀的時候去計(jì)算顏色和繪制弧形
在使用上习贫,這個Drawable
需要提供start
(開始轉(zhuǎn)),stop
(停止轉(zhuǎn))千元,setPercent
(現(xiàn)在應(yīng)該轉(zhuǎn)到哪里)苫昌。
而且我們有時候需要根據(jù)Drawable
是不是正在跑動畫而做某些事情,就是說我們還需要一個isRunning
方法幸海,MaterialDrawable
雖然有了祟身,但它的邏輯可實(shí)現(xiàn)不了我們的訴求,所以要調(diào)整下
而且start
物独,stop
袜硫,isRunning
正好由安卓SDK的接口Animatable
提供了,所以只需要實(shí)現(xiàn)其即可(其實(shí)start
挡篓,stop
的邏輯MaterialDrawable
已經(jīng)做好了婉陷,這里都不需要改,就isRunning
改改就好了)
boolean isRunning = false;
@Override
public void start() {
mAnimation.reset();
mRing.storeOriginals();
// Already showing some part of the ring
if (mRing.getEndTrim() != mRing.getStartTrim()) {
mParent.startAnimation(mFinishAnimation);
} else {
mRing.setColorIndex(0);
mRing.resetOriginals();
mParent.startAnimation(mAnimation);
}
isRunning = true;
}
@Override
public void stop() {
mParent.clearAnimation();
mFinishAnimation.cancel();
mAnimation.cancel();
mFinishAnimation.reset();
mAnimation.reset();
setRotation(0);
mRing.setShowArrow(false);
mRing.setColorIndex(0);
mRing.resetOriginals();
isRunning = false;
}
@Override
public boolean isRunning() {
return isRunning;
}
但是有個地方要注意
原作中的
MaterialDrawable
是根據(jù)內(nèi)置的mTop
變量在draw
的時候移動自己的畫布瞻凤,為何要這樣呢憨攒?因?yàn)樗褪峭ㄟ^這種方式移動下拉refreshView
的,就是說其實(shí)refreshView
沒動阀参,上面的Drawable
的畫布自己動肝集,視覺上跟我們直接讓refreshView
動是一樣的(個人覺得,這種方式維護(hù)起來很容易亂蛛壳,Drawable
只要負(fù)責(zé)好自己的動畫就行啦)
另外我們還需要確定這個Drawable
的高度杏瞻,這里設(shè)置為40dp,mDiameter = dp2px(40);
然后我們要把這個RingDrawable
(姑且就取這個名字)加到我們的刷新控件里面去衙荐,因?yàn)槲覀兊?code>refreshView和loadView
就是ImageView
捞挥,所以直接setImageDrawable
就可以設(shè)置Drawable
了。而且這兩個ImageView
的高度就是DRAW_VIEW_MAX_HEIGHT
(這里是64dp)
refreshView = new ImageView(getContext());
loadView = new ImageView(getContext());
refreshView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp2px(DRAW_VIEW_MAX_HEIGHT)));
loadView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp2px(DRAW_VIEW_MAX_HEIGHT)));
mRefreshDrawable = new RingDrawable(this);
mLoadDrawable = new RingDrawable(this);
refreshView.setImageDrawable(mRefreshDrawable);
loadView.setImageDrawable(mLoadDrawable);
但是如同我們上面所說忧吟,RingDrawable
的高度是40dp砌函,ImageView
的高度是64dp,怎么讓RingDrawable
居中顯示在ImageView
上面呢?直接設(shè)置padding
就可以了
refreshView.setPadding(0, dp2px(DRAW_PADDING), 0, dp2px(DRAW_PADDING));
loadView.setPadding(0, dp2px(DRAW_PADDING), 0, dp2px(DRAW_PADDING));
這里的DRAW_PADDING
就是12dp讹俊,至于寬度垦沉,不需要管,原本的MaterialDrawable
里面計(jì)算的就是橫向屏幕正中間
接下來就該具體使用RingDrawable
了仍劈。
- 一般刷新控件都有這么一個表現(xiàn)厕倍,手指拽動的時候會根據(jù)當(dāng)前位置動態(tài)的設(shè)置動畫,比如傳統(tǒng)的pullToRefresh贩疙,拽到一定位置那個下拉箭頭就變成上拉讹弯,并且提示你松手后開始刷新,我們這里也是如此这溅,所以我們需要監(jiān)聽move事件组民,并且動態(tài)設(shè)置
RingDrawable
的進(jìn)度 - 當(dāng)我們調(diào)用
setLoading
和setRefreshing
之后,就需要根據(jù)具體情況來通知RingDrawable
開始/結(jié)束動畫
動態(tài)設(shè)置進(jìn)度
首先得確定進(jìn)度該怎么計(jì)算芍躏,為了簡單起見邪乍,我們就直接用當(dāng)前位置contentTop
占整個最大可滑動區(qū)域的比重,來作為進(jìn)度
public boolean onTouchEvent(MotionEvent event) {
// .....
switch (action) {
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == -1) {
return true;
}
float originalDragPercent = (float) Math.abs(consignor.contentTop()) / (float)DRAG_MAX_RANGE + .4f;
mDragPercent = Math.min(1f, Math.abs(originalDragPercent));
consignor.setDrawPercent(mDragPercent);
consignor.dragHelper().processTouchEvent(event);
break;
// .....
}
// .....
}
為啥要加個0.4f对竣?咳咳庇楞,這個是因?yàn)?code>MaterialDrawable計(jì)算的需要,有時間可以看看它的算法
然后是setDrawPercent
的實(shí)現(xiàn)
mRefreshDrawable.setPercent(drawPercent);
mLoadDrawable.setPercent(drawPercent);
之前說了否纬,RingDrawable
是通過draw
來繪制的吕晌,所以設(shè)置了進(jìn)度之后不要忘記invalidate
mRefreshDrawable.invalidateSelf();
mLoadDrawable.invalidateSelf();
然后是把RingDrawable
與setLoading
和setRefreshing
聯(lián)系起來
前面所講,通過setLoading
來設(shè)置刷新狀態(tài)或者取消刷新狀態(tài)的時候临燃,其實(shí)是用的scroller來讓整個刷新控件滾動的,那么就是通過computeScroll
來計(jì)算滾完沒有膜廊,同時會在移動之后調(diào)用VDH的onViewPositionChanged
(參見《用viewDragHelper來寫刷新控件<一>》)
那么自然是要在滾完之后才開啟/關(guān)閉動畫播放
public void computeScroll() {
animContinue = dragHelper.continueSettling(true);
if (animContinue) {
ViewCompat.postInvalidateOnAnimation(this);
mRefreshDrawable.invalidateSelf();
mLoadDrawable.invalidateSelf();
} else {
if (ScrollStatus.isRefreshing(status)) {
mRefreshDrawable.start();
} else if (ScrollStatus.isLoading(status)) {
mLoadDrawable.start();
} else if (ScrollStatus.isIdle(status)) {
mRefreshDrawable.stop();
mLoadDrawable.stop();
}
}
}
這樣寫之后我們就能控制好開啟/關(guān)閉動畫播放了嗎乏沸?
不
computeScroll
有個特點(diǎn),就是每次view
在調(diào)用draw
的時候都會去調(diào)它爪瓜,所以不能簡單的判斷animContinue
為false
蹬跃,因?yàn)檫@樣我們甚至還在拖拽,它的邏輯依然會進(jìn)入下面那部分铆铆。于是我們需要一個標(biāo)志位lastAnimState
來區(qū)別
public void setRefreshing(boolean refreshing) {
if (refreshing) {
lastAnimState = true;
if (dragHelper.smoothSlideViewTo(mTarget, 0, dp2px(DRAW_VIEW_MAX_HEIGHT))) {
ViewCompat.postInvalidateOnAnimation(this);
status = ScrollStatus.REFRESHING;
} else {
status = ScrollStatus.REFRESHING;
}
} else {
lastAnimState = true;
if (dragHelper.smoothSlideViewTo(mTarget, 0, 0)) {
ViewCompat.postInvalidateOnAnimation(this);
status = ScrollStatus.IDLE;
} else {
status = ScrollStatus.IDLE;
}
}
}
public void computeScroll() {
animContinue = dragHelper.continueSettling(true);
if (animContinue && lastAnimState == animContinue) {
ViewCompat.postInvalidateOnAnimation(this);
mRefreshDrawable.invalidateSelf();
mLoadDrawable.invalidateSelf();
} else if (!animContinue && lastAnimState != animContinue) {
if (ScrollStatus.isRefreshing(status)) {
mRefreshDrawable.start();
} else if (ScrollStatus.isLoading(status)) {
mLoadDrawable.start();
} else if (ScrollStatus.isIdle(status)) {
mRefreshDrawable.stop();
mLoadDrawable.stop();
}
lastAnimState = animContinue;
}
}
看上去好像沒有問題了蝶缀,但是請?jiān)O(shè)想這么一種情況:
在本來就是刷新狀態(tài)的前提下,我們向下拉動薄货,松手的一瞬間再次去拉動翁都,這時候它還沒有滾到該到的位置,那么這時候status是什么狀態(tài)呢谅猾?是
ScrollStatus.DRAGGING
因?yàn)椴还芪覀儎倓偡攀值臅r候它是不是ScrollStatus.REFRESHING
柄慰,我們現(xiàn)在是在拖動它鳍悠,所以它已經(jīng)被設(shè)置為ScrollStatus.DRAGGING
了,可是這會造成什么問題先煎?
因?yàn)槲覀儎倓偸撬墒至嗽羯云鋵?shí)程序走向是進(jìn)入到了setRefreshing
,這意味著lastAnimState
被置為了true
薯蝎,然而我們又接著繼續(xù)開始拖動,前面我們說到谤绳,在view
調(diào)用draw
的時候都會進(jìn)入computeScroll
占锯,就是說接下來我們又會進(jìn)入computeScroll
!而且這時候animContinue
為false
缩筛,lastAnimState
為true
消略,然后我們就可能得面臨著動畫被錯誤開啟的境況了mRefreshDrawable.start();
(如果這時候還沒有被設(shè)置為ScrollStatus.DRAGGING
的話),用戶于是會看到瞎抛,我拖拽的時候那個動畫還在跑艺演,而且跑的那么怪異(setPercent
和start
同時起作用)。桐臊。胎撤。
所以我們得繼續(xù)修正,這里用另外一個status來避免這種干擾
public void setRefreshing(boolean refreshing) {
if (refreshing) {
lastAnimState = true;
if (dragHelper.smoothSlideViewTo(mTarget, 0, dp2px(DRAW_VIEW_MAX_HEIGHT))) {
ViewCompat.postInvalidateOnAnimation(this);
scrollStatus = ScrollStatus.REFRESHING;
} else {
status = ScrollStatus.REFRESHING;
scrollStatus = status;
}
} else {
lastAnimState = true;
if (dragHelper.smoothSlideViewTo(mTarget, 0, 0)) {
ViewCompat.postInvalidateOnAnimation(this);
scrollStatus = ScrollStatus.IDLE;
} else {
status = ScrollStatus.IDLE;
scrollStatus = status;
}
}
}
public void computeScroll() {
animContinue = dragHelper.continueSettling(true);
if (animContinue && lastAnimState == animContinue) {
ViewCompat.postInvalidateOnAnimation(this);
mRefreshDrawable.invalidateSelf();
mLoadDrawable.invalidateSelf();
} else if (!animContinue && lastAnimState != animContinue) {
if (ScrollStatus.isRefreshing(scrollStatus)) {
mRefreshDrawable.start();
} else if (ScrollStatus.isLoading(scrollStatus)) {
mLoadDrawable.start();
} else if (ScrollStatus.isIdle(scrollStatus)) {
mRefreshDrawable.stop();
mLoadDrawable.stop();
}
status = scrollStatus;
lastAnimState = animContinue;
}
}
說到這里断凶,就還有另外一種情況伤提,我們當(dāng)前是刷新狀態(tài),然后開始拖拽认烁,那么我們就需要先把已經(jīng)在運(yùn)轉(zhuǎn)的動畫停住肿男,所以我們的setDrawPercent
也要做些調(diào)整
public void setDrawPercent(float drawPercent) {
if (mRefreshDrawable.isRunning()) {
lastAnimState = false;
mRefreshDrawable.stop();
}
if (mLoadDrawable.isRunning()) {
lastAnimState = false;
mLoadDrawable.stop();
}
mRefreshDrawable.setPercent(drawPercent);
mLoadDrawable.setPercent(drawPercent);
mRefreshDrawable.invalidateSelf();
mLoadDrawable.invalidateSelf();
}
如此一來,動畫已經(jīng)添加完成却嗡,加載部分代碼跟刷新是差不多的舶沛,就不贅述了
接下來,我們還剩下監(jiān)聽回調(diào)窗价,來讓刷新控件真正的可被使用到項(xiàng)目之中
監(jiān)聽回調(diào)
首先如庭,我們得先確定由刷新控件暴露哪些回調(diào)接口
一般我們使用的刷新控件,往往有如下幾個接口:
- 刷新回調(diào)
onRefresh
- 加載回調(diào)
onLoad
- 刷新取消
refreshCancel
- 加載取消
loadCancel
- 設(shè)置模式
setMode
設(shè)置模式分為允許刷新舌镶,允許加載柱彻,不允許刷新,不允許加載
以上幾個接口是一個刷新控件最基本的接口餐胀,仔細(xì)看看哟楷,其實(shí)分為兩種,一種是刷新的否灾,一種是加載的
public interface DragLoadListener {
void onLoad();
void loadCancel();
}
public interface DragRefreshListener {
void onRefresh();
void refreshCancel();
}
至于setMode
這里簡化一下卖擅,直接根據(jù)refreshListener
和loadListener
是否為null來進(jìn)行判斷
@Override
public boolean isRefreshAble() {
return refreshListener != null;
}
@Override
public boolean isLoadAble() {
return loadListener != null;
}
那么何時響應(yīng)onRefresh
和refreshCancel
呢?必然是在computeScroll
和setRefreshing
之中做文章
onRefresh
一般我們在調(diào)用setRefreshing(true)
就會觸發(fā)onRefresh
,然而一個更精準(zhǔn)的觸發(fā)時機(jī)應(yīng)該是在整個computeScroll
滾動結(jié)束的時候
public void computeScroll() {
animContinue = dragHelper.continueSettling(true);
if (animContinue && lastAnimState == animContinue) {
ViewCompat.postInvalidateOnAnimation(this);
mRefreshDrawable.invalidateSelf();
mLoadDrawable.invalidateSelf();
} else if (!animContinue && lastAnimState != animContinue) {
if (ScrollStatus.isRefreshing(scrollStatus)) {
mRefreshDrawable.start();
if (isRefreshAble()) {
refreshListener.onRefresh();
}
} else if (ScrollStatus.isLoading(scrollStatus)) {
mLoadDrawable.start();
if (isLoadAble()) {
loadListener.onLoad();
}
} else if (ScrollStatus.isIdle(scrollStatus)) {
mRefreshDrawable.stop();
mLoadDrawable.stop();
// 取消刷新或者加載回調(diào)
}
status = scrollStatus;
lastAnimState = animContinue;
}
}
當(dāng)然如果本身已經(jīng)不能滾動惩阶,則直接觸發(fā)
if (refreshing) {
lastAnimState = true;
if (dragHelper.smoothSlideViewTo(mTarget, 0, dp2px(DRAW_VIEW_MAX_HEIGHT))) {
ViewCompat.postInvalidateOnAnimation(this);
scrollStatus = ScrollStatus.REFRESHING;
} else {
status = ScrollStatus.REFRESHING;
scrollStatus = status;
// 不必滑動挎狸,直接觸發(fā)
if (isRefreshAble()) {
refreshListener.onRefresh();
}
}
} else {
lastAnimState = true;
if (dragHelper.smoothSlideViewTo(mTarget, 0, 0)) {
ViewCompat.postInvalidateOnAnimation(this);
scrollStatus = ScrollStatus.IDLE;
} else {
status = ScrollStatus.IDLE;
scrollStatus = status;
}
}
onLoad
的部分跟這個差不多,就不說了
refreshCancel
同樣的断楷,refreshCancel
也是在computeScroll
滾動結(jié)束的時候觸發(fā)
public void computeScroll() {
.....
mRefreshDrawable.stop();
mLoadDrawable.stop();
refreshListener.refreshCancel();
......
}
public void setRefreshing(boolean refreshing) {
if (refreshing) {
......
} else {
lastAnimState = true;
if (dragHelper.smoothSlideViewTo(mTarget, 0, 0)) {
ViewCompat.postInvalidateOnAnimation(this);
scrollStatus = ScrollStatus.IDLE;
} else {
if (ScrollStatus.isRefreshing(scrollStatus) && isRefreshAble()) {
refreshListener.refreshCancel();
}
status = ScrollStatus.IDLE;
scrollStatus = status;
}
}
}
可是問題來了锨匆,請注意一下setRefreshing(false)
和setLoading(false)
的邏輯部分
public void setLoading(boolean loading, boolean animation) {
if (loading) {
....
} else {
lastAnimState = true;
if (dragHelper.smoothSlideViewTo(mTarget, 0, 0)) {
ViewCompat.postInvalidateOnAnimation(this);
scrollStatus = ScrollStatus.IDLE;
} else {
if (ScrollStatus.isLoading(scrollStatus) && isLoadAble()) {
loadListener.loadCancel();
}
status = ScrollStatus.IDLE;
scrollStatus = status;
}
}
}
它們都是通過dragHelper.smoothSlideViewTo(mTarget, 0, 0)
的方式將刷新控件復(fù)位,而我們的computeScroll
都是通過ScrollStatus.isIdle(scrollStatus)
來停止動畫并且調(diào)用cancel回調(diào)冬筒,因此我們必須要某種方式來區(qū)分到底是refreshCancel
還是loadCancel
恐锣。這里我們加一個方向變量,來標(biāo)識是哪個方向的cancel舞痰,暫確定為UP
表示refresh
土榴,DOWN
表示load
Direction smoothToDirection = Direction.STATIC;
public void setRefreshing(boolean refreshing) {
if (refreshing) {
......
} else {
lastAnimState = true;
if (dragHelper.smoothSlideViewTo(mTarget, 0, 0)) {
ViewCompat.postInvalidateOnAnimation(this);
scrollStatus = ScrollStatus.IDLE;
smoothToDirection = Direction.UP;
} else {
if (ScrollStatus.isRefreshing(scrollStatus) && isRefreshAble()) {
refreshListener.refreshCancel();
}
status = ScrollStatus.IDLE;
scrollStatus = status;
}
}
}
public void computeScroll() {
.....
mRefreshDrawable.stop();
mLoadDrawable.stop();
if (smoothToDirection == Direction.UP && isRefreshAble()) {
refreshListener.refreshCancel();
}
if (smoothToDirection == Direction.DOWN && isLoadAble()) {
loadListener.loadCancel();
}
smoothToDirection = Direction.STATIC;
......
}
public void setLoading(boolean loading, boolean animation) {
if (loading) {
....
} else {
lastAnimState = true;
if (dragHelper.smoothSlideViewTo(mTarget, 0, 0)) {
ViewCompat.postInvalidateOnAnimation(this);
scrollStatus = ScrollStatus.IDLE;
smoothToDirection = Direction.DOWN;
} else {
if (ScrollStatus.isLoading(scrollStatus) && isLoadAble()) {
loadListener.loadCancel();
}
status = ScrollStatus.IDLE;
scrollStatus = status;
}
}
}
現(xiàn)在可以正常的并且精準(zhǔn)的響應(yīng)cancel
了嗎?還不夠响牛。
事實(shí)上玷禽,還有一種情況會調(diào)用
setRefreshing(false)
和setLoading(false)
,那就是在靜止?fàn)顟B(tài)的時候呀打,手機(jī)稍微拖拽一下刷新控件矢赁,控件是拖動中,但拖拽的距離并沒有達(dá)到刷新/加載的反應(yīng)閾值聚磺,這時候松手刷新控件只會滾回靜止位置
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
if (contentTop > dp2px(DRAW_VIEW_MAX_HEIGHT)) {
setRefreshing(true);
} else if (contentTop < -dp2px(DRAW_VIEW_MAX_HEIGHT)) {
setLoading(true);
} else if (contentTop > 0) {
setRefreshing(false);
} else if (contentTop == 0) {
// 松手后通過setRefreshing(false)或者setLoading(false)滾回靜止位置
if (!ScrollViewCompat.canSmoothDown(mTarget)) {
setRefreshing(false);
} else if (!ScrollViewCompat.canSmoothUp(mTarget)) {
setLoading(false);
}
} else {
setLoading(false);
}
}
但在我們代碼里面還是會觸發(fā)cancel
調(diào)用坯台,顯然這是不合理的。因此我們必須要在開始拖拽的時刻就判斷出當(dāng)前是不是刷新中或者加載中的狀態(tài)瘫寝,這里我們需要在ACTION_DOWN事件的時候就判斷蜒蕾,回到DragDelegate
來:
public boolean onInterceptTouchEvent(MotionEvent event) {
final int action = MotionEventCompat.getActionMasked(event);
switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = MotionEventCompat.getPointerId(event, 0);
initY = (int) MotionEventUtil.getMotionEventY(event, mActivePointerId);
consignor.dragHelper().shouldInterceptTouchEvent(event);
mDragPercent = 0;
// 通知刷新控件去判斷
consignor.beforeMove();
break;
.......
}
然后我們刷新控件實(shí)現(xiàn)beforeMove
方法:
boolean shouldCancel = false;
public void beforeMove() {
shouldCancel = ScrollStatus.isRefreshing(status) || ScrollStatus.isLoading(status);
}
computeScroll
方法調(diào)整:
public void computeScroll() {
.....
mRefreshDrawable.stop();
mLoadDrawable.stop();
if (smoothToDirection == Direction.UP && isRefreshAble() && shouldCancel) {
refreshListener.refreshCancel();
}
if (smoothToDirection == Direction.DOWN && isLoadAble() && shouldCancel) {
loadListener.loadCancel();
}
smoothToDirection = Direction.STATIC;
shouldCancel = false;
......
}
終于,刷新控件基本完成焕阿,已基本實(shí)現(xiàn)了下拉刷新咪啡,上拉加載功能,效果圖如下:
到了這里其實(shí)該控件可以結(jié)束了暮屡,最后一章將討論一下工具類的實(shí)現(xiàn)撤摸,以及增強(qiáng)功能emptyView
的支持