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

十一期間,大家都去玩耍了哮兰,筆者以前寫的一個(gè)開源庫收到了star,筆者非常高興苟弛,心血來潮之下喝滞,決定重新搞一搞,耗費(fèi)了三天的假期膏秫。筆者期望右遭,這個(gè)刷新控件能像Google的SwipeRefreshLayout一樣,支持大多數(shù)列表控件,另外還得有加載更多功能窘哈,最好要很方便的支持個(gè)性化吧吹榴。開源庫在這,TwinklingRefreshLayout滚婉,如果喜歡請(qǐng)star图筹,筆者的文章也是圍繞著這個(gè)控件的實(shí)現(xiàn)來說的。

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

1. View的onAttachedToWindow()方法

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

此時(shí)View被添加到了窗體上,View有了一個(gè)用于顯示的Surface,將開始繪制。因此其保證了在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樹中遍布開了吁脱,而visibility并不會(huì)對(duì)這個(gè)方法產(chǎn)生影響桑涎。

  • onAttachedToWindow方法是在Activity 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方法是在Activity 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來說,Header和Footer需要及時(shí)顯示出來,View又沒有明顯的生命周期,因此在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)用在Activity或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ā)過程由dispatchTouchEvent漂洋、onInterceptTouchEvent和onTouchEvent三個(gè)方法來共同完成的遥皂。由于事件的傳遞是自頂向下的力喷,對(duì)于ViewGroup,筆者覺得最重要的就是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來交接處理滑動(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)糊余,也就是用戶在下拉可能想要刷新的狀態(tài)和用戶在上拉可能想要加載更多的狀態(tài)秀又。也就是上面state記錄的狀態(tài)。接下來的事情就簡(jiǎn)單咯贬芥,監(jiān)聽一下ACTION_MOVE和ACTION_UP就好了吐辙。

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

在ACTION_UP時(shí)壹甥,需要判斷子View的位移有沒有達(dá)到進(jìn)入刷新或者是加載更多狀態(tài)的要求救巷,即mChildView.getTranslationY() >= mHeadHeight - mTouchSlop,mTouchSlop是為了防止發(fā)生抖動(dòng)而存在句柠。判斷進(jìn)入了刷新狀態(tài)時(shí)浦译,當(dāng)前子View的位移在HeadHeight和maxHeadHeight之間棒假,所以需要讓子View的位移回到HeadHeight處,否則就直接回到0處精盅。

5. Interpolator插值器

Interpolator用于動(dòng)畫中的時(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è)值越來越大,通過decelerateInterpolator獲取到的插值也越來越大粘驰,只不過這些值的變化量是越來越小(decelerate效果)屡谐。dy表示的是手指移動(dòng)的距離。這只是筆者為了滑動(dòng)的柔和性使用的一種計(jì)算方式蝌数,頭部位移的最大距離是mWaveHeight = dy/2愕掏,這樣看的話可以發(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.01.0之間的float參數(shù)饵撑,0.0代表動(dòng)畫的開始,1.0代表動(dòng)畫的結(jié)束唆貌。返回值則可以超過1.0滑潘,也可以小于0.0,比如OvershotInterpolator锨咙。所以getInterpolation()是用來實(shí)現(xiàn)輸入01返回0~1左右的函數(shù)值的一個(gè)函數(shù)语卤。

6. 屬性動(dòng)畫

上面說到了手指抬起的時(shí)候,mChildView的位移要么回到mHeadHeight處酪刀,要么回到0處粱侣。直接setTranslationY()不免太不友好,所以我們這里使用屬性動(dòng)畫來做蓖宦。本來是直接可以用mChildView.animate()方法來完成屬性動(dòng)畫的齐婴,因?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)畫只能夠?qū)崿F(xiàn)移動(dòng)稠茂、縮放柠偶、旋轉(zhuǎn)和淡入淡出這四種動(dòng)畫操作,而且它只是改變了View的顯示效果睬关,改變了畫布繪制出來的樣子诱担,而不會(huì)真正去改變View的屬性。比如用補(bǔ)間動(dòng)畫對(duì)一個(gè)按鈕進(jìn)行了移動(dòng)电爹,只有在原位置點(diǎn)擊按鈕才會(huì)發(fā)生響應(yīng)蔫仙,而屬性動(dòng)畫則可以真正的移動(dòng)按鈕。屬性動(dòng)畫最簡(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來控制心的當(dāng)前縮放值大小就是個(gè)不錯(cuò)的選擇施籍。除此之外居扒,還可以調(diào)用setStartDelay()方法來設(shè)置動(dòng)畫延遲播放的時(shí)間,調(diào)用setRepeatCount()和setRepeatMode()方法來設(shè)置動(dòng)畫循環(huán)播放的次數(shù)以及循環(huán)播放的模式等丑慎。

如果想要實(shí)現(xiàn)View的位移喜喂,ValueAnimator顯然是比較麻煩的,我們可以使用ValueAnimator的子類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里面找有沒有 getAbc()setAbc(...) 這兩個(gè)方法,如果沒有捂掰,動(dòng)畫就沒有效果,它內(nèi)部應(yīng)該是處理了相應(yīng)的異常曾沈。另外還可以用AnimatorSet來實(shí)現(xiàn)多個(gè)屬性動(dòng)畫同時(shí)播放这嚣,也可以在xml中寫屬性動(dòng)畫。

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

要實(shí)現(xiàn)個(gè)性化的Header和Footer塞俱,最最重要的當(dāng)然是把滑動(dòng)過程中系數(shù)都回調(diào)出來啦姐帚。在ACTION_MOVE的時(shí)候,在ACTION_UP的時(shí)候障涯,還有在mChildView在執(zhí)行屬性動(dòng)畫的時(shí)候罐旗,而且mChildView當(dāng)前所處的狀態(tài)都是很明確的,寫個(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()是下拉過程中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í)則超過了HeadLayout的高度,其最大高度可以到達(dá) mWaveHeight/mHeadHeight匙姜。這樣我們只需要寫一個(gè)View來實(shí)現(xiàn)這個(gè)接口就可以實(shí)現(xiàn)個(gè)性化了畅厢,該有的參數(shù)都有了!

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

不能在手指快速滾動(dòng)到頂部時(shí)對(duì)越界做出反饋氮昧,這是一個(gè)繼承及ViewGroup的刷新控件的通病框杜。沒有繼承自具體的列表控件浦楣,它沒辦法獲取到列表控件的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è)工具類GestureDetector去處理选调,它可以輔助檢測(cè)用戶的單擊夹供、滑動(dòng)、長(zhǎng)按仁堪、雙擊哮洽、快速滑動(dòng)等行為。我們這里只需要重寫onFling()方法并獲取到手指在Y方向上的速度velocityY弦聂,要是再能及時(shí)的發(fā)現(xiàn)mChildView滾動(dòng)到了頂部就可以解決問題了鸟辅。

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ù)來實(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)畫。這樣的策略也還有一些缺陷烤镐,不能獲取到mChildView滾動(dòng)到頂部時(shí)的滾動(dòng)速度蛋济,也就不能根據(jù)不同的滾動(dòng)速度來實(shí)現(xiàn)更加友好的越界效果。現(xiàn)在的越界高度是固定的炮叶,還需要后面進(jìn)行優(yōu)化瘫俊,比如采用加速度來計(jì)算,是否可行還待驗(yàn)證悴灵。

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

上面的方法對(duì)于RecyclerView和AbsListView都好用扛芽,對(duì)于ScrollView、WebView就頭疼了积瞒,只能使用延時(shí)計(jì)算一段時(shí)間看有沒有到達(dá)頂部的方式來判斷的策略川尖。延時(shí)策略的思想就是通過發(fā)送一系列的延時(shí)消息從而達(dá)到一種漸進(jìn)式計(jì)算的效果,具體來說可以使用Handler或View的postDelayed方法茫孔,也可以使用線程的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,由于這些沒有實(shí)現(xiàn)OnScrollListener接口,無法回調(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)到頂部或者用戶又進(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; //這里沒有break,寫作-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沒有滾動(dòng)到邊界處艇潭,則會(huì)在10ms之后向自己發(fā)送一條MSG_CONTINUE_COMPUTE_SCROLL的消息,然后繼續(xù)進(jìn)行判斷戏蔑。然后在合適的時(shí)候越界回彈就好了蹋凝。

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

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

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

  2. getView()方法中返回this

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

@Override
protected 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所示效果尚胞,可以具體查看筆者的開源庫TwinklingRefreshLayout

總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末允青,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子卵沉,更是在濱河造成了極大的恐慌颠锉,老刑警劉巖法牲,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異琼掠,居然都是意外死亡拒垃,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門瓷蛙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悼瓮,“玉大人,你說我怎么就攤上這事速挑“担” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵姥宝,是天一觀的道長(zhǎng)翅萤。 經(jīng)常有香客問我,道長(zhǎng)腊满,這世上最難降的妖魔是什么套么? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮碳蛋,結(jié)果婚禮上胚泌,老公的妹妹穿的比我還像新娘。我一直安慰自己肃弟,他們只是感情好玷室,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著笤受,像睡著了一般穷缤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上箩兽,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天津肛,我揣著相機(jī)與錄音,去河邊找鬼汗贫。 笑死身坐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的落包。 我是一名探鬼主播部蛇,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼咐蝇!你這毒婦竟也來了搪花?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎撮竿,沒想到半個(gè)月后吮便,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡幢踏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年髓需,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片房蝉。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡僚匆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出搭幻,到底是詐尸還是另有隱情咧擂,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布檀蹋,位于F島的核電站松申,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏俯逾。R本人自食惡果不足惜贸桶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望桌肴。 院中可真熱鬧皇筛,春花似錦、人聲如沸坠七。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽彪置。三九已至拄踪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間悉稠,已是汗流浹背宫蛆。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工艘包, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留的猛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓想虎,卻偏偏與公主長(zhǎng)得像卦尊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子舌厨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,303評(píng)論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫岂却、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,121評(píng)論 4 61
  • Apple Pay運(yùn)行環(huán)境:iPhone6以上設(shè)備,操作系統(tǒng)最低iOS9.0以上躏哩,部分信息設(shè)置需要iOS9.2以上...
    Yasin的簡(jiǎn)書閱讀 44,406評(píng)論 78 130
  • 在閱讀《好好學(xué)習(xí)》這本書之前署浩,跟著永澄老師進(jìn)行了檢視閱讀,粗閱讀后扫尺,深深的被這本書折服了筋栋,從來沒有哪一本書能夠讓我...
    smallfen閱讀 298評(píng)論 0 0
  • 很多人憶起童年都是說快樂或不快樂弊攘,而我的童年,是帶有味道的姑曙,各種中藥的味道襟交,西藥的味道,還有千層餅和“雞腿”的香味...
    冷小卡閱讀 250評(píng)論 0 0