精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

想要親手實現一個刷新控件,你只需要掌握這些知識

移動開發 Android
現在Android陣營里面的刷新控件很多,稂莠不齊。筆者試圖從不一樣的角度,在它的個性化和滾動上下一些功夫。筆者期望,這個刷新控件能像Google的SwipeRefreshLayout一樣,支持大多數列表控件,有加載更多功能,最好是要很方便的支持個性化,滾動中能夠越界是不是也會帶來比普通的刷新控件更好的交互體驗。

現在Android陣營里面的刷新控件很多,稂莠不齊。筆者試圖從不一樣的角度,在它的個性化和滾動上下一些功夫。筆者期望,這個刷新控件能像Google的SwipeRefreshLayout一樣,支持大多數列表控件,有加載更多功能,最好是要很方便的支持個性化,滾動中能夠越界是不是也會帶來比普通的刷新控件更好的交互體驗。開源庫在這,TwinklingRefreshLayout,如果喜歡請star,筆者的文章也是圍繞著這個控件的實現來說的。

為了方便,筆者將TwinklingRefreshLayout直接繼承自FrameLayout而不是ViewGroup,可以省去onMeasure、onLayout等一些麻煩,Header和Footer則是通過LayoutParams來設置View的Gravity屬性來做的。

1. View的onAttachedToWindow()方法

首先View沒有明顯的生命周期,我們又不能再構造函數里面addView()給控件添加頭部和底部,因此這個操作比較合適的時機就是在onDraw()之前——onAttachedToWindow()方法中。

此時View被添加到了窗體上,View有了一個用于顯示的Surface,將開始繪制。因此其保證了在onDraw()之前調用,但可能在調用 onDraw(Canvas) 之前的任何時刻,包括調用 onMeasure(int, int) 之前或之后。比較適合去執行一些初始化操作。(此外在屏蔽Home鍵的時候也會回調這個方法)

  • onDetachedFromWindow()與onAttachedToWindow()方法相對應。
  • ViewGroup先是調用自己的onAttachedToWindow()方法,再調用其每個child的onAttachedToWindow()方法,這樣此方法就在整個view樹中遍布開了,而visibility并不會對這個方法產生影響。
  • onAttachedToWindow方法是在Activity resume的時候被調用的,也就是act對應的window被添加的時候,且每個view只會被調用一次,父view的調用在前,不論view的visibility狀態都會被調用,適合做些view特定的初始化操作;
  • onDetachedFromWindow方法是在Activity destroy的時候被調用的,也就是act對應的window被刪除的時候,且每個view只會被調用一次,父view的調用在后,也不論view的visibility狀態都會被調用,適合做最后的清理操作;

就TwinklingRefreshLayout來說,Header和Footer需要及時顯示出來,View又沒有明顯的生命周期,因此在onAttachedToWindow()中進行設置可以保證在onDraw()之前添加了刷新控件。

  1. @Override 
  2.     protected void onAttachedToWindow() { 
  3.         super.onAttachedToWindow(); 
  4.  
  5.         //添加頭部 
  6.         FrameLayout headViewLayout = new FrameLayout(getContext()); 
  7.         LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0); 
  8.         layoutParams.gravity = Gravity.TOP
  9.         headViewLayout.setLayoutParams(layoutParams); 
  10.  
  11.         mHeadLayout = headViewLayout; 
  12.         this.addView(mHeadLayout);//addView(view,-1)添加到-1的位置 
  13.  
  14.         //添加底部 
  15.         FrameLayout bottomViewLayout = new FrameLayout(getContext()); 
  16.         LayoutParams layoutParams2 = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0); 
  17.         layoutParams2.gravity = Gravity.BOTTOM; 
  18.         bottomViewLayout.setLayoutParams(layoutParams2); 
  19.  
  20.         mBottomLayout = bottomViewLayout; 
  21.         this.addView(mBottomLayout); 
  22.         //...其它步驟 
  23.     }  

但是當TwinklingRefreshLayout應用在Activity或Fragment中時,可能會因為執行onResume重新觸發了onAttachedToWindow()方法而導致重復創建Header和Footer擋住原先添加的View,因此需要加上判斷:

  1. @Override 
  2.    protected void onAttachedToWindow() { 
  3.        super.onAttachedToWindow(); 
  4.        System.out.println("onAttachedToWindow綁定窗口"); 
  5.  
  6.        //添加頭部 
  7.        if (mHeadLayout == null) { 
  8.            FrameLayout headViewLayout = new FrameLayout(getContext()); 
  9.            LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0); 
  10.            layoutParams.gravity = Gravity.TOP
  11.            headViewLayout.setLayoutParams(layoutParams); 
  12.  
  13.            mHeadLayout = headViewLayout; 
  14.  
  15.            this.addView(mHeadLayout);//addView(view,-1)添加到-1的位置 
  16.  
  17.            if (mHeadView == null) setHeaderView(new RoundDotView(getContext())); 
  18.        } 
  19.        //... 
  20.    }  

2. View的事件分發機制

事件的分發過程由dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三個方法來共同完成的。由于事件的傳遞是自頂向下的,對于ViewGroup,筆者覺得最重要的就是onInterceptTouchEvent方法了,它關系到事件是否能夠繼續向下傳遞。看如下偽代碼:

  1. public boolean dispatchTouchEvent(MotionEvenet ev){ 
  2.     boolean consume = false
  3.     if (onInterceptTouchEvent(ev)) { 
  4.         consume = onTouchEvent(ev); 
  5.     }else
  6.         consume = child.dispatchTouchEvent(ev); 
  7.     } 
  8.     return consume; 
  9.  

如代碼所示,如果ViewGroup攔截了(onInterceptTouchEvent返回true)事件,則事件會在ViewGroup的onTouchEvent方法中消費,而不會傳到子View;否則事件將交給子View去分發。

我們需要做的就是在子View滾動到頂部或者底部時及時的攔截事件,讓ViewGroup的onTouchEvent來交接處理滑動事件。

3. 判斷子View滾動達到邊界

在什么時候對事件進行攔截呢?對于Header,當手指向下滑動也就是 dy>0 且子View已經滾動到頂部(不能再向上滾動)時攔截;對于bottom則是 dy<0 且子View已經滾動到底部(不能再向下滾動)時攔截:

  1. @Override 
  2.     public boolean onInterceptTouchEvent(MotionEvent ev) { 
  3.         switch (ev.getAction()) { 
  4.             case MotionEvent.ACTION_DOWN: 
  5.                 mTouchY = ev.getY(); 
  6.                 break; 
  7.             case MotionEvent.ACTION_MOVE: 
  8.                 float dy = ev.getY() - mTouchY; 
  9.  
  10.                 if (dy > 0 && !canChildScrollUp()) { 
  11.                     state = PULL_DOWN_REFRESH; 
  12.                     return true
  13.                 } else if (dy < 0 && !canChildScrollDown() && enableLoadmore) { 
  14.                     state = PULL_UP_LOAD; 
  15.                     return true
  16.                 } 
  17.                 break; 
  18.         } 
  19.         return super.onInterceptTouchEvent(ev); 
  20.     }  

判斷View能不能繼續向上滾動,對于sdk14以上版本,v4包里提供了方法:

  1. public boolean canChildScrollUp() { 
  2.     return ViewCompat.canScrollVertically(mChildView, -1); 
  3.  

其它情況,直接交給子View了,ViewGroup這里也管不著。

4. ViewGroup 的 onTouchEvent 方法

走到這一步,子View的滾動已經交給子View自己去搞了,ViewGroup需要處理的事件只有兩個臨界狀態,也就是用戶在下拉可能想要刷新的狀態和用戶在上拉可能想要加載更多的狀態。也就是上面state記錄的狀態。接下來的事情就簡單咯,監聽一下ACTION_MOVE和ACTION_UP就好了。

首先在ACTION_DOWN時需要記錄下最原先的手指按下的位置 mTouchY,然后在一系列ACTION_MOVE過程中,獲取當前位移(ev.getY()-mTouchY),然后通過 某種計算方式 不斷計算當前的子View應該位移的距離offsetY,調用mChildView.setTranslationY(offsetY)來不斷設置子View的位移,同時需要給HeadLayout申請布局高度來完成頂部控件的顯示。這其中筆者使用的計算方式就是插值器(Interpolator)。

在ACTION_UP時,需要判斷子View的位移有沒有達到進入刷新或者是加載更多狀態的要求,即mChildView.getTranslationY() >= mHeadHeight - mTouchSlop,mTouchSlop是為了防止發生抖動而存在。判斷進入了刷新狀態時,當前子View的位移在HeadHeight和maxHeadHeight之間,所以需要讓子View的位移回到HeadHeight處,否則就直接回到0處。

5. Interpolator插值器

Interpolator用于動畫中的時間插值,其作用就是把0到1的浮點值變化映射到另一個浮點值變化。上面提到的計算方式如下:

  1. float offsetY = decelerateInterpolator.getInterpolation(dy / mWaveHeight / 2) * dy / 2; 

其中(dy / mWaveHeight / 2)是一個0~1之間的浮點值,隨著下拉高度的增加,這個值越來越大,通過decelerateInterpolator獲取到的插值也越來越大,只不過這些值的變化量是越來越小(decelerate效果)。dy表示的是手指移動的距離。這只是筆者為了滑動的柔和性使用的一種計算方式,頭部位移的最大距離是mWaveHeight = dy/2,這樣看的話可以發現 dy / mWaveHeight / 2 會從0到1變化。Interpolator繼承自TimeInterpolator接口,源碼如下:

  1. public interface TimeInterpolator { 
  2.     /** 
  3.      * Maps a value representing the elapsed fraction of an animation to a value that represents 
  4.      * the interpolated fraction. This interpolated value is then multiplied by the change in 
  5.      * value of an animation to derive the animated value at the current elapsed animation time
  6.      * 
  7.      * @param input A value between 0 and 1.0 indicating our current point 
  8.      *        in the animation where 0 represents the start and 1.0 represents 
  9.      *        the end 
  10.      * @return The interpolation value. This value can be more than 1.0 for 
  11.      *         interpolators which overshoot their targets, or less than 0 for 
  12.      *         interpolators that undershoot their targets. 
  13.      */ 
  14.     float getInterpolation(float input); 
  15.  

getInterpolation接收一個0.0~1.0之間的float參數,0.0代表動畫的開始,1.0代表動畫的結束。返回值則可以超過1.0,也可以小于0.0,比如OvershotInterpolator。所以getInterpolation()是用來實現輸入0~1返回0~1左右的函數值的一個函數。  

 

6. 屬性動畫

上面說到了手指抬起的時候,mChildView的位移要么回到mHeadHeight處,要么回到0處。直接setTranslationY()不免太不友好,所以我們這里使用屬性動畫來做。本來是直接可以用mChildView.animate()方法來完成屬性動畫的,因為需要兼容低版本并回調一些參數,所以這里使用ObjectAnimator:

  1. private void animChildView(float endValue, long duration) { 
  2.         ObjectAnimator oa = ObjectAnimator.ofFloat(mChildView, "translationY", mChildView.getTranslationY(), endValue); 
  3.         oa.setDuration(duration); 
  4.         oa.setInterpolator(new DecelerateInterpolator());//設置速率為遞減 
  5.         oa.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 
  6.             @Override 
  7.             public void onAnimationUpdate(ValueAnimator animation) { 
  8.                 int height = (int) mChildView.getTranslationY();//獲得mChildView當前y的位置 
  9.                 height = Math.abs(height); 
  10.  
  11.                 mHeadLayout.getLayoutParams().height = height; 
  12.                 mHeadLayout.requestLayout(); 
  13.             } 
  14.         }); 
  15.     oa.start(); 
  16.  

傳統的補間動畫只能夠實現移動、縮放、旋轉和淡入淡出這四種動畫操作,而且它只是改變了View的顯示效果,改變了畫布繪制出來的樣子,而不會真正去改變View的屬性。比如用補間動畫對一個按鈕進行了移動,只有在原位置點擊按鈕才會發生響應,而屬性動畫則可以真正的移動按鈕。屬性動畫最簡單的一種使用方式就是使用ValueAnimator:

  1. ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);   
  2. anim.start();  

它可以傳入多個參數,如ValueAnimator.ofFloat(0f, 5f, 3f, 10f),他會根據設置的插值器依次計算,比如想做一個心跳的效果,用ValueAnimator來控制心的當前縮放值大小就是個不錯的選擇。除此之外,還可以調用setStartDelay()方法來設置動畫延遲播放的時間,調用setRepeatCount()和setRepeatMode()方法來設置動畫循環播放的次數以及循環播放的模式等。

如果想要實現View的位移,ValueAnimator顯然是比較麻煩的,我們可以使用ValueAnimator的子類ObjectAnimator,如下:

  1. ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "alpha", 1f, 0f, 1f);   
  2. animator.setDuration(5000);   
  3. animator.start();    

傳入的第一個值是Object,不局限于View,傳入的第二個參數為Object的一個屬性,比如傳入"abc",ObjectAnimator會去Object里面找有沒有 getAbc() 和 setAbc(...) 這兩個方法,如果沒有,動畫就沒有效果,它內部應該是處理了相應的異常。另外還可以用AnimatorSet來實現多個屬性動畫同時播放,也可以在xml中寫屬性動畫。

7. 個性化Header和Footer的接口

要實現個性化的Header和Footer,最最重要的當然是把滑動過程中系數都回調出來啦。在ACTION_MOVE的時候,在ACTION_UP的時候,還有在mChildView在執行屬性動畫的時候,而且mChildView當前所處的狀態都是很明確的,寫個接口就好了。

  1. public interface IHeaderView { 
  2.     View getView(); 
  3.  
  4.     void onPullingDown(float fraction,float maxHeadHeight,float headHeight); 
  5.  
  6.     void onPullReleasing(float fraction,float maxHeadHeight,float headHeight); 
  7.  
  8.     void startAnim(float maxHeadHeight,float headHeight); 
  9.  
  10.     void onFinish(); 
  11.  

getView()方法保證在TwinklingRefreshLayout中可以取到在外部設置的View,onPullingDown()是下拉過程中ACTION_MOVE時的回調方法,onPullReleasing()是下拉狀態中ACTION_UP時的回調方法,startAnim()則是正在刷新時回調的方法。其中fraction=mChildView.getTranslationY()/mHeadHeight,fraction=1 時,mChildView的位移恰好是HeadLayout的高度,fraction>1 時則超過了HeadLayout的高度,其最大高度可以到達 mWaveHeight/mHeadHeight。這樣我們只需要寫一個View來實現這個接口就可以實現個性化了,該有的參數都有了!

8. 實現越界回彈

不能在手指快速滾動到頂部時對越界做出反饋,這是一個繼承及ViewGroup的刷新控件的通病。沒有繼承自具體的列表控件,它沒辦法獲取到列表控件的Scroller,不能獲取到列表控件的當前滾動速度,更是不能預知列表控件什么時候能滾動到頂部;同時ViewGroup除了達到臨界狀態的事件被攔截了,其它事件全都交給了子View去處理。我們能獲取到的有關于子View的操作,只有簡簡單單的手指的觸摸事件。so, let's do it!

  1. mChildView.setOnTouchListener(new OnTouchListener() { 
  2.     @Override 
  3.     public boolean onTouch(View v, MotionEvent event) { 
  4.         return gestureDetector.onTouchEvent(event); 
  5.     } 
  6. });  

我們把在mChildView上的觸摸事件交給了一個工具類GestureDetector去處理,它可以輔助檢測用戶的單擊、滑動、長按、雙擊、快速滑動等行為。我們這里只需要重寫onFling()方法并獲取到手指在Y方向上的速度velocityY,要是再能及時的發現mChildView滾動到了頂部就可以解決問題了。

  1. GestureDetector gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { 
  2.  
  3.         @Override 
  4.         public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 
  5.             mVelocityY = velocityY; 
  6.         } 
  7.     });  

此外獲取速度還可以用VelocityTracker,比較麻煩一些:

  1. VelocityTracker tracker = VelocityTracker.obtain(); 
  2. tracker.addMovement(ev); 
  3. //然后在恰當的位置使用如下方法獲取速度 
  4. tracker.computeCurrentVelocity(1000); 
  5. mVelocityY = (int)tracker.getYVelocity();  

繼續來實現越界回彈。對于RecyclerView、AbsListView,它們提供有OnScrollListener可以獲取一下滾動狀態:

  1. if (mChildView instanceof RecyclerView) { 
  2.             ((RecyclerView) mChildView).addOnScrollListener(new RecyclerView.OnScrollListener() { 
  3.                 @Override 
  4.                 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 
  5.                     if (!isRefreshing && !isLoadingmore && newState == RecyclerView.SCROLL_STATE_IDLE) { 
  6.                         if (mVelocityY >= 5000 && ScrollingUtil.isRecyclerViewToTop((RecyclerView) mChildView)) { 
  7.                             animOverScrollTop(); 
  8.                         } 
  9.                         if (mVelocityY <= -5000 && ScrollingUtil.isRecyclerViewToBottom((RecyclerView) mChildView)) { 
  10.                             animOverScrollBottom(); 
  11.                         } 
  12.                     } 
  13.                     super.onScrollStateChanged(recyclerView, newState); 
  14.                 } 
  15.             }); 
  16.         }  

筆者選取了一個滾動速度的臨界值,Y方向的滾動速度大于5000時才允許越界回彈,RecyclerView的OnScrollListener可以讓我們獲取到滾動狀態的改變,滾動到頂部時滾動完成,狀態變為SCROLL_STATE_IDLE,執行越界回彈動畫。這樣的策略也還有一些缺陷,不能獲取到mChildView滾動到頂部時的滾動速度,也就不能根據不同的滾動速度來實現更加友好的越界效果。現在的越界高度是固定的,還需要后面進行優化,比如采用加速度來計算,是否可行還待驗證。

9. 滾動的延時計算策略

上面的方法對于RecyclerView和AbsListView都好用,對于ScrollView、WebView就頭疼了,只能使用延時計算一段時間看有沒有到達頂部的方式來判斷的策略。延時策略的思想就是通過發送一系列的延時消息從而達到一種漸進式計算的效果,具體來說可以使用Handler或View的postDelayed方法,也可以使用線程的sleep方法。另外提一點,需要不斷循環計算一個數值,比如自定義View需要實現根據某個數值變化的動效,最好不要使用Thread + while 循環的方式計算,使用ValueAnimator會是更好的選擇。這里筆者選擇了Handler的方式。

  1. @Override 
  2. public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 
  3.     mVelocityY = velocityY; 
  4.     if (!(mChildView instanceof AbsListView || mChildView instanceof RecyclerView)) { 
  5.         //既不是AbsListView也不是RecyclerView,由于這些沒有實現OnScrollListener接口,無法回調狀態,只能采用延時策略 
  6.         if (Math.abs(mVelocityY) >= 5000) { 
  7.             mHandler.sendEmptyMessage(MSG_START_COMPUTE_SCROLL); 
  8.         } else { 
  9.             cur_delay_times = ALL_DELAY_TIMES; 
  10.         } 
  11.     } 
  12.     return false
  13.  

在滾動速度大于5000的時候發送一個重新計算的消息,Handler收到消息后,延時一段時間繼續給自己發送消息,直到時間用完或者mChildView滾動到頂部或者用戶又進行了一次Fling動作。 

  1. private Handler mHandler = new Handler() { 
  2.     @Override 
  3.     public void handleMessage(Message msg) { 
  4.         switch (msg.what) { 
  5.             case MSG_START_COMPUTE_SCROLL: 
  6.                 cur_delay_times = -1; //這里沒有break,寫作-1方便計數 
  7.             case MSG_CONTINUE_COMPUTE_SCROLL: 
  8.                 cur_delay_times++; 
  9.  
  10.                 if (!isRefreshing && !isLoadingmore && mVelocityY >= 5000 && childScrollToTop()) { 
  11.                     animOverScrollTop(); 
  12.                     cur_delay_times = ALL_DELAY_TIMES; 
  13.                 } 
  14.  
  15.                 if (!isRefreshing && !isLoadingmore && mVelocityY <= -5000 && childScrollToBottom()) { 
  16.                     animOverScrollBottom(); 
  17.                     cur_delay_times = ALL_DELAY_TIMES; 
  18.                 } 
  19.  
  20.                 if (cur_delay_times < ALL_DELAY_TIMES) 
  21.                     mHandler.sendEmptyMessageDelayed(MSG_CONTINUE_COMPUTE_SCROLL, 10); 
  22.                 break; 
  23.             case MSG_STOP_COMPUTE_SCROLL: 
  24.                 cur_delay_times = ALL_DELAY_TIMES; 
  25.                 break; 
  26.         } 
  27.     } 
  28. };  

ALL_DELAY_TIMES是最多可以計算的次數,當Handler接收到MSG_START_COMPUTE_SCROLL消息時,如果mChildView沒有滾動到邊界處,則會在10ms之后向自己發送一條MSG_CONTINUE_COMPUTE_SCROLL的消息,然后繼續進行判斷。然后在合適的時候越界回彈就好了。

10. 實現個性化Header

這里筆者來演示一下,怎么輕輕松松的做一個個性化的Header,比如新浪微博樣式的刷新Header(如下面第1圖)。

  1. 創建 SinaRefreshView 繼承自 FrameLayout 并實現 IHeaderView 接口
  2. getView()方法中返回this
  3. 在onAttachedToWindow()方法中獲取一下需要用到的布局(筆者寫到了xml中,也可以直接在代碼里面寫)
    1. @Override 
    2. protected void onAttachedToWindow() { 
    3.     super.onAttachedToWindow(); 
    4.  
    5.     if (rootView == null) { 
    6.         rootView = View.inflate(getContext(), R.layout.view_sinaheader, null); 
    7.         refreshArrow = (ImageView) rootView.findViewById(R.id.iv_arrow); 
    8.         refreshTextView = (TextView) rootView.findViewById(R.id.tv); 
    9.         loadingView = (ImageView) rootView.findViewById(R.id.iv_loading); 
    10.         addView(rootView); 
    11.     } 
  4. 實現其它方法
    1. @Override 
    2. public void onPullingDown(float fraction, float maxHeadHeight, float headHeight) { 
    3.     if (fraction < 1f) refreshTextView.setText(pullDownStr); 
    4.     if (fraction > 1f) refreshTextView.setText(releaseRefreshStr); 
    5.     refreshArrow.setRotation(fraction * headHeight / maxHeadHeight * 180); 
    6.  
    7. @Override 
    8. public void onPullReleasing(float fraction, float maxHeadHeight, float headHeight) { 
    9.     if (fraction < 1f) { 
    10.         refreshTextView.setText(pullDownStr); 
    11.         refreshArrow.setRotation(fraction * headHeight / maxHeadHeight * 180); 
    12.         if (refreshArrow.getVisibility() == GONE) { 
    13.             refreshArrow.setVisibility(VISIBLE); 
    14.             loadingView.setVisibility(GONE); 
    15.         } 
    16.     } 
    17.  
    18. @Override 
    19. public void startAnim(float maxHeadHeight, float headHeight) { 
    20.     refreshTextView.setText(refreshingStr); 
    21.     refreshArrow.setVisibility(GONE); 
    22.     loadingView.setVisibility(VISIBLE); 
  5. 布局文件
    1. <?xml version="1.0" encoding="utf-8"?> 
    2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    3.     android:orientation="horizontal" android:layout_width="match_parent" 
    4.     android:layout_height="match_parent" 
    5.     android:gravity="center"
    6.     <ImageView 
    7.         android:id="@+id/iv_arrow" 
    8.         android:layout_width="wrap_content" 
    9.         android:layout_height="wrap_content" 
    10.         android:src="@drawable/ic_arrow"/> 
    11.  
    12.     <ImageView 
    13.         android:id="@+id/iv_loading" 
    14.         android:visibility="gone" 
    15.         android:layout_width="34dp" 
    16.         android:layout_height="34dp" 
    17.         android:src="@drawable/anim_loading_view"/> 
    18.  
    19.     <TextView 
    20.         android:id="@+id/tv" 
    21.         android:layout_width="wrap_content" 
    22.         android:layout_height="wrap_content" 
    23.         android:layout_marginLeft="16dp" 
    24.         android:textSize="16sp" 
    25.         android:text="下拉刷新"/> 
    26. </LinearLayout> 

注意fraction的使用,比如上面的代碼 refreshArrow.setRotation(fraction headHeight / maxHeadHeight 180),fraction * headHeight表示當前頭部滑動的距離,然后算出它和最大高度的比例,然后乘以180,可以使得在滑動到最大距離時Arrow恰好能旋轉180度。startAnim()方法是在onRefresh之后會自動調用的方法。

要想實現如圖2所示效果,可以具體查看筆者的開源庫TwinklingRefreshLayout。

 

 

總結

至此,筆者實現這個刷新控件的所有核心思想都講完了,其中并沒有用到多么高深的技術,只是需要我們多一點耐心,多去調試,不要逃避bug,多挑戰一下自己。

責任編輯:龐桂玉 來源: segmentfault
相關推薦

2018-08-03 12:21:02

2018-01-09 15:44:57

2018-01-05 15:36:12

工具博客寫作

2021-04-16 09:17:39

機器學習人工智能AI

2020-07-21 08:42:16

搞垮服務器日志

2021-10-20 07:48:17

DatalistCSS技巧

2022-08-24 16:26:51

Linuxcheat 命令

2019-10-28 11:30:43

架構數據結構布隆過濾器

2022-11-04 13:06:47

JVMJava程序

2019-10-31 10:43:05

Python 開發編程語言

2021-08-02 07:23:54

爬蟲requests網絡庫

2020-04-01 11:12:43

腦機接口機器翻譯人工智能

2025-02-08 10:29:03

2018-07-26 08:42:11

2020-02-25 23:36:04

代碼開發工具

2012-11-12 09:44:43

2020-10-24 20:10:40

Python 開發編程語言

2019-06-10 13:50:08

Linux命令shell

2021-03-09 16:36:21

大數據薪資工作

2019-04-16 08:50:56

WebHTTP緩存
點贊
收藏

51CTO技術棧公眾號

黑人玩欧美人三根一起进| 秋霞av一区二区三区| 日韩一二三区| 婷婷久久综合九色国产成人| 青娱乐一区二区| 中文字幕视频二区| 亚洲天堂偷拍| 在线成人一区二区| 男人添女人荫蒂国产| 中文字幕资源网在线观看免费 | 欧美亚洲国产bt| 欧美另类z0zxhd电影| 97久久精品人人澡人人爽缅北| 久久成人激情视频| 国产激情一区| 在线免费av一区| 国产黄色激情视频| 91涩漫在线观看| 99久久久国产精品免费蜜臀| 成人国产亚洲精品a区天堂华泰| 日本熟妇一区二区| 久久精品亚洲欧美日韩精品中文字幕| 日韩av影片在线观看| 天天干天天色天天干| 亚洲黄色免费av| 亚洲一区二区三区自拍| 一区二区三区视频| 欧洲伦理片一区 二区 三区| 国产jizzjizz一区二区| 国产精品视频中文字幕91| 久久露脸国语精品国产91| 91精品综合| 色哟哟入口国产精品| 亚洲av无码国产精品久久| 日韩精品一区二区三区中文在线 | 国产一区二区网站| 日本不卡在线视频| 欧美一级电影免费在线观看| 国产精彩视频在线观看| 亚洲影视一区| 久久九九精品99国产精品| 黄色片网站免费| 在线观看欧美理论a影院| 日韩激情在线视频| 国产伦精品一区二区三区88av| 伊人久久大香伊蕉在人线观看热v 伊人久久大香线蕉综合影院首页 伊人久久大香 | 国产精品av久久久久久麻豆网| 中文字幕日韩电影| 精品无码国产污污污免费网站| 国产亚洲精品熟女国产成人| 美女100%一区| 岛国av在线不卡| 成人午夜视频在线观看免费| 日本片在线看| 亚洲一区电影777| 日韩精品一区二区在线视频| 青草青在线视频| 亚洲久草在线视频| www.69av| av中文在线资源库| 亚洲www啪成人一区二区麻豆| 欧美亚洲色图视频| av2020不卡| 欧美性色xo影院| 国产日韩成人内射视频 | 一区二区在线影院| 不卡av电影院| 免费看一级一片| 亚洲精品色图| 国产91色在线免费| 最近中文字幕在线免费观看| 另类小说综合欧美亚洲| 成人激情春色网| 国产极品久久久| 成人av资源在线| 日本一区免费观看| 免费成人黄色| 亚洲国产裸拍裸体视频在线观看乱了 | 97人妻天天摸天天爽天天| 在线视频亚洲专区| 日韩在线中文视频| 麻豆视频在线观看| 亚洲欧美高清| 国产欧美精品一区二区三区-老狼| 一本一道人人妻人人妻αv| 国模娜娜一区二区三区| 成人av播放| 艳母动漫在线看| 国产精品久久久久影视| 亚洲av首页在线| 破处女黄色一级片| 一本一道波多野毛片中文在线| 国产精品免费久久久久| 穿情趣内衣被c到高潮视频| sm久久捆绑调教精品一区| 欧美在线免费观看视频| 一级做a爱视频| 欧美自拍视频| 久热精品视频在线| 亚洲s码欧洲m码国产av| 国产精品一区专区| 日韩福利影院| segui88久久综合| 欧美日韩国产综合一区二区| 精品久久久久一区二区| 日韩精品dvd| 久久免费观看视频| 在线免费观看视频网站| av中文字幕亚洲| 亚洲视频小说| 欧美裸体视频| 日韩一区二区三区四区五区六区| 中文字幕狠狠干| 欧美喷水视频| 国产日产欧美a一级在线| 午夜国产在线视频| 亚洲黄色性网站| 性chinese极品按摩| 欧美五码在线| 久久久久久久久亚洲| 国产有码在线观看| 国产日产精品一区| 男女激情无遮挡| 中文字幕一区图| 久久夜色精品国产欧美乱| 夜夜爽妓女8888视频免费观看| 成人h动漫精品一区二区| 18视频在线观看娇喘| 国产福利一区二区三区在线播放| 亚洲黄页网在线观看| 麻豆视频在线免费看| 国产成人手机高清在线观看网站| 日本免费观看视| 在线亚洲免费| 国产超碰91| 手机在线免费观看av| 欧美精品久久久久久久多人混战| 中文字幕丰满孑伦无码专区| 1024日韩| 狠狠色噜噜狠狠狠狠97| 99热99热| 成人三级网址| 欧美另类z0zxhd电影| 日韩av片在线免费观看| 石原莉奈在线亚洲三区| 久久亚洲免费| 成人欧美大片| 日韩国产精品视频| 91精品国产乱码在线观看| 国产91精品久久久久久久网曝门| 婷婷视频在线播放| 亚洲精品大片| 久久亚洲精品中文字幕冲田杏梨| 国产绿帽一区二区三区| 亚洲美女屁股眼交| 亚洲成人福利视频| 在线欧美三区| 免费亚洲精品视频| 亚洲第一av| 亚洲片在线资源| 免费av中文字幕| 国产欧美日韩激情| 一本色道久久亚洲综合精品蜜桃| 91视频综合| 91观看网站| h片在线观看视频免费| 亚洲国产天堂网精品网站| 日韩精品无码一区二区| 91女神在线视频| 国产精品入口免费软件| 色欧美自拍视频| 91精品国产91久久久久青草| 波多野结衣在线高清| 亚洲精品成人免费| 樱花视频在线免费观看| 国产精品国产三级国产aⅴ入口| 奇米影视四色在线| 欧美精品国产| 精品一区国产| 国产精品久久久久久久久久齐齐 | 久久精品电影| 在线免费观看成人网| 综合欧美亚洲| 欧美专区中文字幕| 黄网页免费在线观看| 亚洲精品一区二区三区福利 | 国产黄a三级三级| 国产精品白丝av| 91精品国产综合久久蜜臀| 中文字幕色网站| 欧美视频官网| 日韩免费电影一区二区三区| 久久在线观看| 日韩女在线观看| 中文字幕有码在线观看| 日韩精品视频免费在线观看| 一级黄色片在线播放| 午夜日韩在线电影| 免费黄色激情视频| 99re在线视频这里只有精品| 日韩av在线中文| 亚洲一区黄色| 国产香蕉一区二区三区| 伊人久久大香线蕉无限次| 亚洲a中文字幕| 韩漫成人漫画| 欧美高清一级大片| av女优在线| 日韩成人在线免费观看| 国产精品一二三四五区| 图片区小说区国产精品视频| 中国1级黄色片| 久久久夜色精品亚洲| 中文字幕1区2区| 奇米精品一区二区三区四区| 91猫先生在线| 国产精品分类| 老汉色影院首页| 欧美伦理在线视频| 久久久久综合一区二区三区| 日本在线视频一区二区三区| 国产精品入口尤物| 台湾佬成人网| 456亚洲影院| 金瓶狂野欧美性猛交xxxx| xxxx欧美18另类的高清| 成人免费在线视频网| 国产网站欧美日韩免费精品在线观看| av小说天堂网| 69堂成人精品免费视频| 中文字幕你懂的| 在线中文字幕一区| 中文字幕69页| 福利视频导航一区| 亚洲国产精品午夜在线观看| 亚洲欧美二区三区| 亚洲最大的黄色网址| 国产精品久久免费看| 美国黄色特级片| 国产视频一区二区三区在线观看| 少妇精品一区二区| 99久久精品免费| 在线观看国产三级| 97se亚洲国产综合自在线| 国产精品扒开腿做爽爽爽a片唱戏 亚洲av成人精品一区二区三区 | 91蝌蚪porny| 日本不卡视频一区| 国产高清精品在线| 麻豆网站免费观看| 国产在线一区二区| 涩多多在线观看| 国产一区二区三区视频在线播放| 毛片毛片毛片毛| 国产一区二区看久久| 奇米777在线| 国产成人精品免费一区二区| 激情综合激情五月| 99re8在线精品视频免费播放| 性色av蜜臀av色欲av| 久久久亚洲午夜电影| 日本猛少妇色xxxxx免费网站| 中文字幕av免费专区久久| 懂色av蜜臀av粉嫩av永久| 亚洲视频资源在线| 国产又黄又爽又无遮挡| 亚洲午夜视频在线观看| 色婷婷av国产精品| 欧美午夜片在线观看| 亚洲天堂中文在线| 日韩小视频在线观看专区| 亚洲精品无遮挡| 亚洲乱码国产乱码精品精| 波多野结衣一区二区| 精品国产美女在线| 另类视频在线| 日本在线观看天堂男亚洲| 成人精品三级| 3d动漫精品啪啪一区二区三区免费| 亚洲一区二区三区中文字幕在线观看| 国产一区视频观看| 欧美丝袜一区| 国产精品12p| 亚洲毛片在线| 色播五月综合网| 国产91在线观看丝袜| 男人天堂av电影| 亚洲男同性视频| 日产精品久久久| 777亚洲妇女| 午夜在线视频观看| 日韩在线视频观看| 僵尸再翻生在线观看| 国产精品亚洲片夜色在线| 高潮按摩久久久久久av免费| 日本一区视频在线| 黄色日韩在线| 久久99爱视频| 91在线一区二区| 精品国产视频在线观看| 日韩欧美中文免费| 亚洲av无码一区二区三区dv| 国产午夜精品免费一区二区三区| 尤物在线网址| 国产欧美va欧美va香蕉在| 伦理一区二区三区| 精品视频在线免费观看| 欧美成人一区二区三区高清| 日韩欧美精品免费在线| av网站在线免费看| 国产一区二区三区欧美| a级片在线免费观看| 成人一区二区电影| 狠狠操综合网| 久久成人免费观看| 国产精品资源网站| 波多野结衣一二三四区| 午夜av一区二区三区| 国产精品自产拍| 国产亚洲精品美女| 黄色视屏在线免费观看| 亚洲淫片在线视频| 日韩一区二区在线| 国产精彩免费视频| 99re热这里只有精品免费视频| 青娱乐国产盛宴| 3d动漫精品啪啪1区2区免费| 福利片在线观看| 热久久美女精品天天吊色| 久久黄色影视| 国产黄色片免费在线观看| 国产99久久久久| 久热这里有精品| 欧美一级日韩免费不卡| 欧美尤物美女在线| 国产精品久久久久久久久影视| 亚洲第一论坛sis| 免费看一级大黄情大片| 成人毛片视频在线观看| 久久久国产精品人人片| 日韩欧美国产综合一区| 任你弄在线视频免费观看| 亚洲一区二区免费| 亚洲高清影视| 99国产精品免费视频| 亚洲激情图片一区| 亚洲乱色熟女一区二区三区| 九九九久久国产免费| 视频一区视频二区欧美| 久久亚洲国产成人精品无码区| 国产精品亚洲人在线观看| 黄色一级视频免费| 精品国产乱码久久| 2018av在线| 久久综合九九| 日韩经典一区二区| 中文字幕在线观看二区| 9191精品国产综合久久久久久| 激情成人四房播| 成人午夜电影在线播放| 最新亚洲激情| 欧美做受高潮6| 欧美日本一道本| 亚洲资源一区| 国产精品一区视频| 性欧美videos另类喷潮| 丁香激情五月少妇| 777奇米四色成人影色区| 毛片在线网址| 欧美高清视频一区二区三区在线观看| 久久久亚洲人| 成人一级黄色大片| 精品免费国产一区二区三区四区| free性护士videos欧美| 日韩伦理一区二区三区av在线| 精品亚洲porn| 国产无遮挡又黄又爽在线观看| 国产丝袜高跟一区| 丰满人妻一区二区三区大胸| 久久综合九色综合97婷婷女人 | 免费av在线一区二区| 久色成人在线| 国产一区二区播放| 国产视频精品va久久久久久| 国产精品久久久久久久久久齐齐| 狠狠干视频网站| 久久这里都是精品| 一级黄色片视频| 97视频在线观看免费| 欧美日韩激情| 亚洲妇女无套内射精| 色婷婷综合久久久中文字幕| 国产cdts系列另类在线观看| 精品视频一区在线| 九色综合国产一区二区三区| 日本熟妇成熟毛茸茸| 深夜福利日韩在线看| 欧美在线导航| 欧美高清精品一区二区| 91国内精品野花午夜精品| 亚洲91av|