溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

想要親手實(shí)現(xiàn)一個(gè)刷新控件,你只需要掌握這些知識(shí)

發(fā)布時(shí)間:2020-06-05 18:36:46 來(lái)源:網(wǎng)絡(luò) 閱讀:441 作者:lcodecore 欄目:移動(dòng)開(kāi)發(fā)

現(xiàn)在A(yíng)ndroid陣營(yíng)里面的刷新控件很多,稂莠不齊。筆者試圖從不一樣的角度,在它的個(gè)性化和滾動(dòng)上下一些功夫。筆者期望,這個(gè)刷新控件能像Google的SwipeRefreshLayout一樣,支持大多數(shù)列表控件,有加載更多功能,最好是要很方便的支持個(gè)性化,滾動(dòng)中能夠越界是不是也會(huì)帶來(lái)比普通的刷新控件更好的交互體驗(yàn)。開(kāi)源庫(kù)在這,TwinklingRefreshLayout,如果喜歡請(qǐng)star,筆者的文章也是圍繞著這個(gè)控件的實(shí)現(xiàn)來(lái)說(shuō)的。


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

1. View的onAttachedToWindow()方法

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


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


  • onDetachedFromWindow()與onAttachedToWindow()方法相對(duì)應(yīng)。

  • ViewGroup先是調(diào)用自己的onAttachedToWindow()方法,再調(diào)用其每個(gè)child的onAttachedToWindow()方法,這樣此方法就在整個(gè)view樹(shù)中遍布開(kāi)了,而visibility并不會(huì)對(duì)這個(gè)方法產(chǎn)生影響。

  • onAttachedToWindow方法是在A(yíng)ctivity resume的時(shí)候被調(diào)用的,也就是act對(duì)應(yīng)的window被添加的時(shí)候,且每個(gè)view只會(huì)被調(diào)用一次,父view的調(diào)用在前,不論view的visibility狀態(tài)都會(huì)被調(diào)用,適合做些view特定的初始化操作;

  • onDetachedFromWindow方法是在A(yíng)ctivity destroy的時(shí)候被調(diào)用的,也就是act對(duì)應(yīng)的window被刪除的時(shí)候,且每個(gè)view只會(huì)被調(diào)用一次,父view的調(diào)用在后,也不論view的visibility狀態(tài)都會(huì)被調(diào)用,適合做最后的清理操作;


就TwinklingRefreshLayout來(lái)說(shuō),Header和Footer需要及時(shí)顯示出來(lái),View又沒(méi)有明顯的生命周期,因此在onAttachedToWindow()中進(jìn)行設(shè)置可以保證在onDraw()之前添加了刷新控件。


    @Override
   protected void onAttachedToWindow() {
       super.onAttachedToWindow();

       //添加頭部
       FrameLayout headViewLayout = new FrameLayout(getContext());
       LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
       layoutParams.gravity = Gravity.TOP;
       headViewLayout.setLayoutParams(layoutParams);

       mHeadLayout = headViewLayout;
       this.addView(mHeadLayout);//addView(view,-1)添加到-1的位置

       //添加底部
       FrameLayout bottomViewLayout = new FrameLayout(getContext());
       LayoutParams layoutParams2 = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
       layoutParams2.gravity = Gravity.BOTTOM;
       bottomViewLayout.setLayoutParams(layoutParams2);

       mBottomLayout = bottomViewLayout;
       this.addView(mBottomLayout);
       //...其它步驟
   }


但是當(dāng)TwinklingRefreshLayout應(yīng)用在A(yíng)ctivity或Fragment中時(shí),可能會(huì)因?yàn)閳?zhí)行onResume重新觸發(fā)了onAttachedToWindow()方法而導(dǎo)致重復(fù)創(chuàng)建Header和Footer擋住原先添加的View,因此需要加上判斷:

    @Override
   protected void onAttachedToWindow() {
       super.onAttachedToWindow();
       System.out.println("onAttachedToWindow綁定窗口");

       //添加頭部
       if (mHeadLayout == null) {
           FrameLayout headViewLayout = new FrameLayout(getContext());
           LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0);
           layoutParams.gravity = Gravity.TOP;
           headViewLayout.setLayoutParams(layoutParams);

           mHeadLayout = headViewLayout;

           this.addView(mHeadLayout);//addView(view,-1)添加到-1的位置

           if (mHeadView == null) setHeaderView(new RoundDotView(getContext()));
       }
       //...
   }


2. View的事件分發(fā)機(jī)制

事件的分發(fā)過(guò)程由dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三個(gè)方法來(lái)共同完成的。由于事件的傳遞是自頂向下的,對(duì)于ViewGroup,筆者覺(jué)得最重要的就是onInterceptTouchEvent方法了,它關(guān)系到事件是否能夠繼續(xù)向下傳遞。看如下偽代碼:

public boolean dispatchTouchEvent(MotionEvenet ev){
   boolean consume = false;
   if (onInterceptTouchEvent(ev)) {
       consume = onTouchEvent(ev);
   }else{
       consume = child.dispatchTouchEvent(ev);
   }
   return consume;
}


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

我們需要做的就是在子View滾動(dòng)到頂部或者底部時(shí)及時(shí)的攔截事件,讓ViewGroup的onTouchEvent來(lái)交接處理滑動(dòng)事件。


3. 判斷子View滾動(dòng)達(dá)到邊界

在什么時(shí)候?qū)κ录M(jìn)行攔截呢?對(duì)于Header,當(dāng)手指向下滑動(dòng)也就是 dy>0 且子View已經(jīng)滾動(dòng)到頂部(不能再向上滾動(dòng))時(shí)攔截;對(duì)于bottom則是 dy<0 且子View已經(jīng)滾動(dòng)到底部(不能再向下滾動(dòng))時(shí)攔截:

@Override
   public boolean onInterceptTouchEvent(MotionEvent ev) {
       switch (ev.getAction()) {
           case MotionEvent.ACTION_DOWN:
               mTouchY = ev.getY();
               break;
           case MotionEvent.ACTION_MOVE:
               float dy = ev.getY() - mTouchY;

               if (dy > 0 && !canChildScrollUp()) {
                   state = PULL_DOWN_REFRESH;
                   return true;
               } else if (dy < 0 && !canChildScrollDown() && enableLoadmore) {
                   state = PULL_UP_LOAD;
                   return true;
               }
               break;
       }
       return super.onInterceptTouchEvent(ev);
   }


判斷View能不能繼續(xù)向上滾動(dòng),對(duì)于sdk14以上版本,v4包里提供了方法:

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


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


4. ViewGroup 的 onTouchEvent 方法

走到這一步,子View的滾動(dòng)已經(jīng)交給子View自己去搞了,ViewGroup需要處理的事件只有兩個(gè)臨界狀態(tài),也就是用戶(hù)在下拉可能想要刷新的狀態(tài)和用戶(hù)在上拉可能想要加載更多的狀態(tài)。也就是上面state記錄的狀態(tài)。接下來(lái)的事情就簡(jiǎn)單咯,監(jiān)聽(tīng)一下ACTION_MOVE和ACTION_UP就好了。


首先在A(yíng)CTION_DOWN時(shí)需要記錄下最原先的手指按下的位置 mTouchY,然后在一系列ACTION_MOVE過(guò)程中,獲取當(dāng)前位移(ev.getY()-mTouchY),然后通過(guò) 某種計(jì)算方式 不斷計(jì)算當(dāng)前的子View應(yīng)該位移的距離offsetY,調(diào)用mChildView.setTranslationY(offsetY)來(lái)不斷設(shè)置子View的位移,同時(shí)需要給HeadLayout申請(qǐng)布局高度來(lái)完成頂部控件的顯示。這其中筆者使用的計(jì)算方式就是插值器(Interpolator)。


在A(yíng)CTION_UP時(shí),需要判斷子View的位移有沒(méi)有達(dá)到進(jìn)入刷新或者是加載更多狀態(tài)的要求,即mChildView.getTranslationY() >= mHeadHeight - mTouchSlop,mTouchSlop是為了防止發(fā)生抖動(dòng)而存在。判斷進(jìn)入了刷新?tīng)顟B(tài)時(shí),當(dāng)前子View的位移在HeadHeight和maxHeadHeight之間,所以需要讓子View的位移回到HeadHeight處,否則就直接回到0處。


5. Interpolator插值器

Interpolator用于動(dòng)畫(huà)中的時(shí)間插值,其作用就是把0到1的浮點(diǎn)值變化映射到另一個(gè)浮點(diǎn)值變化。上面提到的計(jì)算方式如下:

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


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

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


getInterpolation接收一個(gè)0.0~1.0之間的float參數(shù),0.0代表動(dòng)畫(huà)的開(kāi)始,1.0代表動(dòng)畫(huà)的結(jié)束。返回值則可以超過(guò)1.0,也可以小于0.0,比如OvershotInterpolator。所以getInterpolation()是用來(lái)實(shí)現(xiàn)輸入0~1返回0~1左右的函數(shù)值的一個(gè)函數(shù)。



6. 屬性動(dòng)畫(huà)

上面說(shuō)到了手指抬起的時(shí)候,mChildView的位移要么回到mHeadHeight處,要么回到0處。直接setTranslationY()不免太不友好,所以我們這里使用屬性動(dòng)畫(huà)來(lái)做。本來(lái)是直接可以用mChildView.animate()方法來(lái)完成屬性動(dòng)畫(huà)的,因?yàn)樾枰嫒莸桶姹静⒒卣{(diào)一些參數(shù),所以這里使用ObjectAnimator:

private void animChildView(float endValue, long duration) {
       ObjectAnimator oa = ObjectAnimator.ofFloat(mChildView, "translationY", mChildView.getTranslationY(), endValue);
       oa.setDuration(duration);
       oa.setInterpolator(new DecelerateInterpolator());//設(shè)置速率為遞減
       oa.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
           @Override
           public void onAnimationUpdate(ValueAnimator animation) {
               int height = (int) mChildView.getTranslationY();//獲得mChildView當(dāng)前y的位置
               height = Math.abs(height);

               mHeadLayout.getLayoutParams().height = height;
               mHeadLayout.requestLayout();
           }
       });
   oa.start();
}


傳統(tǒng)的補(bǔ)間動(dòng)畫(huà)只能夠?qū)崿F(xiàn)移動(dòng)、縮放、旋轉(zhuǎn)和淡入淡出這四種動(dòng)畫(huà)操作,而且它只是改變了View的顯示效果,改變了畫(huà)布繪制出來(lái)的樣子,而不會(huì)真正去改變View的屬性。比如用補(bǔ)間動(dòng)畫(huà)對(duì)一個(gè)按鈕進(jìn)行了移動(dòng),只有在原位置點(diǎn)擊按鈕才會(huì)發(fā)生響應(yīng),而屬性動(dòng)畫(huà)則可以真正的移動(dòng)按鈕。屬性動(dòng)畫(huà)最簡(jiǎn)單的一種使用方式就是使用ValueAnimator:

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


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


如果想要實(shí)現(xiàn)View的位移,ValueAnimator顯然是比較麻煩的,我們可以使用ValueAnimator的子類(lèi)ObjectAnimator,如下:

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


傳入的第一個(gè)值是Object,不局限于View,傳入的第二個(gè)參數(shù)為Object的一個(gè)屬性,比如傳入"abc",ObjectAnimator會(huì)去Object里面找有沒(méi)有 getAbc()setAbc(...) 這兩個(gè)方法,如果沒(méi)有,動(dòng)畫(huà)就沒(méi)有效果,它內(nèi)部應(yīng)該是處理了相應(yīng)的異常。另外還可以用AnimatorSet來(lái)實(shí)現(xiàn)多個(gè)屬性動(dòng)畫(huà)同時(shí)播放,也可以在xml中寫(xiě)屬性動(dòng)畫(huà)。

7. 個(gè)性化Header和Footer的接口

要實(shí)現(xiàn)個(gè)性化的Header和Footer,最最重要的當(dāng)然是把滑動(dòng)過(guò)程中系數(shù)都回調(diào)出來(lái)啦。在A(yíng)CTION_MOVE的時(shí)候,在A(yíng)CTION_UP的時(shí)候,還有在mChildView在執(zhí)行屬性動(dòng)畫(huà)的時(shí)候,而且mChildView當(dāng)前所處的狀態(tài)都是很明確的,寫(xiě)個(gè)接口就好了。

public interface IHeaderView {
   View getView();

   void onPullingDown(float fraction,float maxHeadHeight,float headHeight);

   void onPullReleasing(float fraction,float maxHeadHeight,float headHeight);

   void startAnim(float maxHeadHeight,float headHeight);

   void onFinish();
}


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


8. 實(shí)現(xiàn)越界回彈

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

mChildView.setOnTouchListener(new OnTouchListener() {
   @Override
   public boolean onTouch(View v, MotionEvent event) {
       return gestureDetector.onTouchEvent(event);
   }});


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

GestureDetector gestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {

       @Override
       public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
           mVelocityY = velocityY;
       }
   });


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

VelocityTracker tracker = VelocityTracker.obtain();
tracker.addMovement(ev);
//然后在恰當(dāng)?shù)奈恢檬褂萌缦路椒ǐ@取速度
tracker.computeCurrentVelocity(1000);
mVelocityY = (int)tracker.getYVelocity();


繼續(xù)來(lái)實(shí)現(xiàn)越界回彈。對(duì)于RecyclerView、AbsListView,它們提供有OnScrollListener可以獲取一下滾動(dòng)狀態(tài):

if (mChildView instanceof RecyclerView) {
           ((RecyclerView) mChildView).addOnScrollListener(new RecyclerView.OnScrollListener() {
               @Override
               public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                   if (!isRefreshing && !isLoadingmore && newState == RecyclerView.SCROLL_STATE_IDLE) {
                       if (mVelocityY >= 5000 && ScrollingUtil.isRecyclerViewToTop((RecyclerView) mChildView)) {
                           animOverScrollTop();
                       }
                       if (mVelocityY <= -5000 && ScrollingUtil.isRecyclerViewToBottom((RecyclerView) mChildView)) {
                           animOverScrollBottom();
                       }
                   }
                   super.onScrollStateChanged(recyclerView, newState);
               }
           });
       }


筆者選取了一個(gè)滾動(dòng)速度的臨界值,Y方向的滾動(dòng)速度大于5000時(shí)才允許越界回彈,RecyclerView的OnScrollListener可以讓我們獲取到滾動(dòng)狀態(tài)的改變,滾動(dòng)到頂部時(shí)滾動(dòng)完成,狀態(tài)變?yōu)镾CROLL_STATE_IDLE,執(zhí)行越界回彈動(dòng)畫(huà)。這樣的策略也還有一些缺陷,不能獲取到mChildView滾動(dòng)到頂部時(shí)的滾動(dòng)速度,也就不能根據(jù)不同的滾動(dòng)速度來(lái)實(shí)現(xiàn)更加友好的越界效果。現(xiàn)在的越界高度是固定的,還需要后面進(jìn)行優(yōu)化,比如采用加速度來(lái)計(jì)算,是否可行還待驗(yàn)證。


9. 滾動(dòng)的延時(shí)計(jì)算策略

上面的方法對(duì)于RecyclerView和AbsListView都好用,對(duì)于ScrollView、WebView就頭疼了,只能使用延時(shí)計(jì)算一段時(shí)間看有沒(méi)有到達(dá)頂部的方式來(lái)判斷的策略。延時(shí)策略的思想就是通過(guò)發(fā)送一系列的延時(shí)消息從而達(dá)到一種漸進(jìn)式計(jì)算的效果,具體來(lái)說(shuō)可以使用Handler或View的postDelayed方法,也可以使用線(xiàn)程的sleep方法。另外提一點(diǎn),需要不斷循環(huán)計(jì)算一個(gè)數(shù)值,比如自定義View需要實(shí)現(xiàn)根據(jù)某個(gè)數(shù)值變化的動(dòng)效,最好不要使用Thread + while 循環(huán)的方式計(jì)算,使用ValueAnimator會(huì)是更好的選擇。這里筆者選擇了Handler的方式。

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
   mVelocityY = velocityY;
   if (!(mChildView instanceof AbsListView || mChildView instanceof RecyclerView)) {
       //既不是AbsListView也不是RecyclerView,由于這些沒(méi)有實(shí)現(xiàn)OnScrollListener接口,無(wú)法回調(diào)狀態(tài),只能采用延時(shí)策略
       if (Math.abs(mVelocityY) >= 5000) {
           mHandler.sendEmptyMessage(MSG_START_COMPUTE_SCROLL);
       } else {
           cur_delay_times = ALL_DELAY_TIMES;
       }
   }
   return false;
}


在滾動(dòng)速度大于5000的時(shí)候發(fā)送一個(gè)重新計(jì)算的消息,Handler收到消息后,延時(shí)一段時(shí)間繼續(xù)給自己發(fā)送消息,直到時(shí)間用完或者mChildView滾動(dòng)到頂部或者用戶(hù)又進(jìn)行了一次Fling動(dòng)作。

private Handler mHandler = new Handler() {
   @Override
   public void handleMessage(Message msg) {
       switch (msg.what) {
           case MSG_START_COMPUTE_SCROLL:
               cur_delay_times = -1; //這里沒(méi)有break,寫(xiě)作-1方便計(jì)數(shù)
           case MSG_CONTINUE_COMPUTE_SCROLL:
               cur_delay_times++;

               if (!isRefreshing && !isLoadingmore && mVelocityY >= 5000 && childScrollToTop()) {
                   animOverScrollTop();
                   cur_delay_times = ALL_DELAY_TIMES;
               }

               if (!isRefreshing && !isLoadingmore && mVelocityY <= -5000 && childScrollToBottom()) {
                   animOverScrollBottom();
                   cur_delay_times = ALL_DELAY_TIMES;
               }

               if (cur_delay_times < ALL_DELAY_TIMES)
                   mHandler.sendEmptyMessageDelayed(MSG_CONTINUE_COMPUTE_SCROLL, 10);
               break;
           case MSG_STOP_COMPUTE_SCROLL:
               cur_delay_times = ALL_DELAY_TIMES;
               break;
       }
   }};


ALL_DELAY_TIMES是最多可以計(jì)算的次數(shù),當(dāng)Handler接收到MSG_START_COMPUTE_SCROLL消息時(shí),如果mChildView沒(méi)有滾動(dòng)到邊界處,則會(huì)在10ms之后向自己發(fā)送一條MSG_CONTINUE_COMPUTE_SCROLL的消息,然后繼續(xù)進(jìn)行判斷。然后在合適的時(shí)候越界回彈就好了。


10. 實(shí)現(xiàn)個(gè)性化Header

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

  1. 創(chuàng)建 SinaRefreshView 繼承自 FrameLayout 并實(shí)現(xiàn) IHeaderView 接口

  2. getView()方法中返回this

  3. 在onAttachedToWindow()方法中獲取一下需要用到的布局(筆者寫(xiě)到了xml中,也可以直接在代碼里面寫(xiě))

@Overrideprotected void onAttachedToWindow() {
   super.onAttachedToWindow();

   if (rootView == null) {
       rootView = View.inflate(getContext(), R.layout.view_sinaheader, null);
       refreshArrow = (ImageView) rootView.findViewById(R.id.iv_arrow);
       refreshTextView = (TextView) rootView.findViewById(R.id.tv);
       loadingView = (ImageView) rootView.findViewById(R.id.iv_loading);
       addView(rootView);
   }}


4.實(shí)現(xiàn)其它方法

@Override
public void onPullingDown(float fraction, float maxHeadHeight, float headHeight) {
   if (fraction < 1f) refreshTextView.setText(pullDownStr);
   if (fraction > 1f) refreshTextView.setText(releaseRefreshStr);
   refreshArrow.setRotation(fraction * headHeight / maxHeadHeight * 180);
}

@Override
public void onPullReleasing(float fraction, float maxHeadHeight, float headHeight) {
   if (fraction < 1f) {
       refreshTextView.setText(pullDownStr);
       refreshArrow.setRotation(fraction * headHeight / maxHeadHeight * 180);
       if (refreshArrow.getVisibility() == GONE) {
           refreshArrow.setVisibility(VISIBLE);
           loadingView.setVisibility(GONE);
       }
   }
}

@Override
public void startAnim(float maxHeadHeight, float headHeight) {
   refreshTextView.setText(refreshingStr);
   refreshArrow.setVisibility(GONE);
   loadingView.setVisibility(VISIBLE);
}

5.布局文件

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:orientation="horizontal" android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:gravity="center">
   <ImageView
       android:id="@+id/iv_arrow"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:src="@drawable/ic_arrow"/>

   <ImageView
       android:id="@+id/iv_loading"
       android:visibility="gone"
       android:layout_width="34dp"
       android:layout_height="34dp"
       android:src="@drawable/anim_loading_view"/>

   <TextView
       android:id="@+id/tv"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_marginLeft="16dp"
       android:textSize="16sp"
       android:text="下拉刷新"/></LinearLayout>


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

要想實(shí)現(xiàn)如圖2所示效果,可以具體查看筆者的開(kāi)源庫(kù)TwinklingRefreshLayout。




總結(jié)

至此,筆者實(shí)現(xiàn)這個(gè)刷新控件的所有核心思想都講完了,其中并沒(méi)有用到多么高深的技術(shù),只是需要我們多一點(diǎn)耐心,多去調(diào)試,不要逃避bug,多挑戰(zhàn)一下自己。


向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI