PullToRefresh原理解析,pulltorefresh解析

來源:互聯網
上載者:User

PullToRefresh原理解析,pulltorefresh解析

代碼屆有一句非常經典的話:“不要重複製造輪子”,多少人看過之後便以此為本,把魯迅的“拿來主義”發揚光大,只搜輪子,不造輪子。但現在我想補充的一句是“不要重複製造輪子,不等於不需要知道輪子是如何製造的”!


讀過PullToRefresh的源碼之後,我便依照著做了一個小Demo出來,下面就此原理為大家解析一番。究竟是哪句代碼實現了如此強大的功能,究竟是哪個方法是貫穿全文上下?


原理:在View中有一個scrollTo方法,可以將整個View移動到指定的位置,PullToRefresh就是重寫了onTouchEvent方法來檢測使用者滑動的位移距離,然後用滑動距離調用scrollTo方法來實現整個View的上下左右移動的。


先:




我的Demo:



手機QQ的是上下可以滑動,我的demo是向上,向下,向左,向右都可以滑動,鬆手之後,自動回到原來的位置。


注,在我的demo裡繼承的是FrameLayout進行的重寫,同樣你也可以選擇重寫LinearLayout或者其他ViewGroup,你可以在新版的手機QQ中看到有大量的布局都支援類似我這種demo的上下滑動(或者叫做彈動)


1.首先來看所需要的變數:

private float mLastMotionX, mLastMotionY; //記錄手指觸摸的位置X,Y座標private float mDeltaX, mDeltaY;//記錄當前手指拉動的X和Y位移量private ScrollToHomeRunnable mScrollToHomeRunnable; //一會兒介紹,用來從位移點回到原點的private enum State{ //當前出於什麼狀態:正在重新整理?水平拉動?垂直拉動?正常狀態?REFRESHING,PULLING_HORIZONTAL,PULLING_VERTICAL,NORMAL,}private enum Orientation{ //記錄拉動的方向:水平?垂直?HORIZONTAL,VERTICAL}private State mState; //目前狀態private Orientation mOrientation; //當前拉動方向

2.初始化方法,初始化的時候只需要將mState 設定為 NORMAL狀態即可。

private void init(Context context){mState = State.NORMAL;}


3-1.重寫onTouchEvent方法,檢測使用者滑動的距離,方向等,然後調用scrollTo來讓整個View位移,這可謂是核心代碼了,先看虛擬碼:

@Overridepublic boolean onTouchEvent(MotionEvent event) {int action = event.getAction();switch(action){case MotionEvent.ACTION_DOWN: /**  * 記錄X,Y座標,恢複mDeltaX和mDeltaY為0  */break;case MotionEvent.ACTION_MOVE:/** * 根據當前觸摸點 - 上次記錄的x或者y座標,得到增量,然後應用到scrollTo方法上去, * 然後重新記錄x,y座標 */break;case MotionEvent.ACTION_UP:/** * 使用者鬆開手指之後,View自動回到位移量為0的位置 */break;}return true;}

3-2:我們先來實現當手指ACTION_DOWN,即剛剛點下的時候,應該做哪些工作?如下代碼所示,其實所做的工作非常簡單,每次只需要重新記錄當前手指的X座標,Y座標,並把X位移量和Y位移量調整為0即可,每次都從最遠點的位置開始拉動。

case MotionEvent.ACTION_DOWN:mLastMotionX = event.getX();mLastMotionY = event.getY();mDeltaY = .0F;mDeltaX = .0F;break;

3-3:當使用者在螢幕上滑動的時候,即ACTION_MOVE的時候,應該記錄使用者此次點擊的X座標(或Y座標)與一開始ACTION_DOWN的時候X座標(或者Y座標)的差值,然後記錄此差值並調用scrollTo方法:

case MotionEvent.ACTION_MOVE:float innerDeltaY = event.getY() - mLastMotionY; //記錄Y的差值float innerDeltaX = event.getX() - mLastMotionX; //記錄X的差值float absInnerDeltaY = Math.abs(innerDeltaY); //Y差值絕對值float absInnerDeltaX = Math.abs(innerDeltaX); //X差值絕對值//當Y差值絕對值 大於 X差值絕對值的時候,我們可以認為使用者正在上下滑動if(absInnerDeltaY > absInnerDeltaX && mState != State.PULLING_HORIZONTAL){mOrientation = Orientation.VERTICAL;//將當前滑動方向置為垂直滑動mState = State.PULLING_VERTICAL;//滑動狀態:正在垂直拉動if(innerDeltaY > 1.0F){ //innerDeltaY為正數,使用者正在向下拉動,1.0F可看做閾值,下面類似mDeltaY -= absInnerDeltaY; //注意這個地方是-=,即累減的過程pull(mDeltaY);}else if(innerDeltaY < -1.0F){ //innerDeltaY為負數,使用者正在向上拉動mDeltaY += absInnerDeltaY; //累加pull(mDeltaY);}//下面的代碼是水平滑動}else if(absInnerDeltaY < absInnerDeltaX && mState != State.PULLING_VERTICAL){mOrientation = Orientation.HORIZONTAL;mState = State.PULLING_HORIZONTAL;if(innerDeltaX > 1.0F){mDeltaX -= absInnerDeltaX;pull(mDeltaX);}else if(innerDeltaX < -1.0F){mDeltaX += absInnerDeltaX;pull(mDeltaX);}}//重新記錄新的座標值mLastMotionX = event.getX();mLastMotionY = event.getY();break;


3-4 大家可能注意到,判斷水平還是垂直移動的時候,有一個mState != State.PULLING_XXX 條件,這個條件是為了限制滑動的方向的,即當使用者正在處於垂直滑動的時候,就禁止使用者水平滑動;當水平滑動的時候就禁止垂直滑動,每次只能按一個方向進行滑動。

在測試的時候,大家即可感受到,向下拉動螢幕的時候,比如位移值為200,那麼如果你想讓螢幕真的位移200,需要調用scrollTo(0, -200),這也是為什麼innerDelta為正值的時候,需要累減;為負值(使用者開始向上滑動),要累加的原因。


3-5 當使用者手指離開的時候,按照PullToRefresh來說就是開始執行使用者的任務(比如重新整理資料等等操作),任務執行完畢之後,View重新調用scrollTo一定的距離(這個距離就是在ACTION_MOVE階段,記錄的mDeltaY和mDeltaX位移量)回到自己原來的位置,在我的demo裡,只是簡單的當使用者手指離開之後就立馬回到原來的位置:

case MotionEvent.ACTION_UP:switch(mOrientation){//根據ACTION_MOVE的時候所確定的方向開始判斷case VERTICAL:smoothScrollTo(mDeltaY);//垂直拉動,讓View重新mDeltaY,重新回到Y的原點break;case HORIZONTAL:smoothScrollTo(mDeltaX);//如果水平拉動,讓View重新滑動mDeltaX,重新回到X的原點break;default:break;}break;

3-6:使用者觸摸階段(ACTION_MOVE)的拉動方法,除以2.0將拉動的距離縮放,這裡代表如果使用者已知從上滑到下面,那麼整個View最多位移半個螢幕。

private void pull(float diff){int value = Math.round(diff / 2.0F);//diff就是位移量,除以2.0相當於一個縮放if(mOrientation == Orientation.VERTICAL){scrollTo(0, value);//注意這裡是核心了,Y方向上移動value距離,X方向上保持不變}else if(mOrientation == Orientation.HORIZONTAL){scrollTo(value, 0);//X方向上移動value距離,Y方向上保持不變}}

3-7:使用者鬆手之後(ACTION_UP)的View自動回到原位置的方法:

private void smoothScrollTo(float diff){int value = Math.round(diff / 2.0F);mScrollToHomeRunnable = new ScrollToHomeRunnable(value, 0);mState = State.REFRESHING;//目前狀態為正在重新整理post(mScrollToHomeRunnable);//view自身有一個post方法,我們提交一個scrollTo的任務給它}

3-8-1:下面來看ScrollToHomeRunnable類的定義,ScrollToHomeRunnable就是用來調用scrollTo的,目的就是要使View從X位移量或者Y位移量返回到初始位置去,那麼為什麼要單獨的把它封裝成一個Runnable類呢?其一是為了調用View自身的View.post(Runnable runnable)和View.postDelayed(Runnable runnable, int delayMills)方法,其二是為了給他一些裝飾效果,比如這裡的減速差值,類比一些特殊的效果。

通過post和postDelayed方法我們可以不停的把這個任務放在View自身的訊息佇列中,以達到不停地調用scrollTo的目的,一旦回到原點之後,我們就停止調用。其實在這裡使用Handler.post(Runnable runnable)也可以實現這樣的操作(不停的把一個Runnable任務添加到訊息佇列中去),大家可以試試,但我測試的是Handler沒有直接View.post(Runnable)平滑性高。


3-8-2:ScrollToHomeRunnable的構造器,很簡單,儲存目標值和當前的位移量,初始化DecelerateInterpolator差值器。

public ScrollToHomeRunnable(int current, int target){this.target = target;this.current = current;mInterpolator = new DecelerateInterpolator();}

3-8-3:ScrollToHomeRunnable的run方法,在這個地方,原PullToRefresh的作者很聰明,知道調用getInterpolation這個方法,來得到一系列的差值點,以此得到一串不同的滑動距離,比如”102,86,67,53,40,32,22,15,9,5,2,0“或者”-178,-157,-130,-107,-84,-65,-49,-35,-22,-13,-6,-2,0“,就是這麼一串值,來讓使用者感覺到這個”突然性的回到了原來的位置“這種感覺。

在規格化時間方面,PullToRefresh的作者想的也很周到,全部都使用的long這種儲存類型,避免使用了float來讓軟體做較多的float計算,他先將時間差值放大1000倍,然後規格化至(0,1000)中的一個值,然後再縮小得到(0,1.0F)中的一個浮點值,併當做參數調用getInterpolation方法,得到下一個需要移動到哪一個位置,以此不停的確定的delta值,就不停的確定current值,只要scrollTo之後,發現current 依然沒有到達target的值,那麼就再次調用postDelayed方法,重新scrollTo。

註:代碼中200這個值代表的意思是:200ms,即經過200ms返回原位置去。

@Overridepublic void run() {if(mStartTime == -1){mStartTime = System.currentTimeMillis();}else{long normalizedTime = (1000 * (System.currentTimeMillis() - mStartTime)) / 200;normalizedTime = Math.max(Math.min(normalizedTime, 1000), 0);final int delta = Math.round((current - target)* mInterpolator.getInterpolation(normalizedTime / 1000f));current = current - delta;if(mOrientation == Orientation.HORIZONTAL){scrollTo(current, 0);//水平scroll}else if(mOrientation == Orientation.VERTICAL){scrollTo(0, current);//垂直scroll}}if(current != target){//沒有回到原點:在經過16毫秒之後繼續postDelayed這個任務postDelayed(this, 16);}else{mState = State.NORMAL;//回到原點,mState置為NORMAL狀態}}

3-9:最後的補充。如果沒有上述的差值器的幫忙,以及不是200ms,或者每次current 只是簡單的遞減的話,使用者可以清楚的看到這個不斷回到原來位置的過程,在這期間,如果使用者點擊的話,將會導致mDeltaX和mDeltaY為0,View會立馬調到原位置去,雖然這裡在特別快的時候來不及點擊,但是還是需要在onTouchEvent一開頭的地方補充上這句代碼,如果當前正處於重新整理狀態,那麼立即返回(禁止使用者點擊):

if(mState == State.REFRESHING){return true;}


3-10:個人QQ:1291700520,Android Programmer. 如轉載、引用等還望註明連結來源,代碼:

https://github.com/anxiaoyi/PullToRefreshTheory



聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.