用viewDragHelper來寫刷新控件<二>

前面第一章我們講了刷新控件的初步實(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)度條镊掖,差不多是這個樣子

material progressbar

從實(shí)現(xiàn)上來說乃戈,它其實(shí)是一個Drawable,就是說其實(shí)是通過不停的draw來達(dá)到動畫的效果的亩进,這一點(diǎn)跟viewdraw是一個道理症虑。正因?yàn)樗蕾?code>draw,所以后面我們會看到归薛,在手勢拖動的過程中谍憔,為了實(shí)時的讓mRefreshDrawablemLoadDrawable發(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)用setLoadingsetRefreshing之后,就需要根據(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();
然后是把RingDrawablesetLoadingsetRefreshing聯(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)它爪瓜,所以不能簡單的判斷animContinuefalse蹬跃,因?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!而且這時候animContinuefalse缩筛,lastAnimStatetrue消略,然后我們就可能得面臨著動畫被錯誤開啟的境況了mRefreshDrawable.start();如果這時候還沒有被設(shè)置為ScrollStatus.DRAGGING的話),用戶于是會看到瞎抛,我拖拽的時候那個動畫還在跑艺演,而且跑的那么怪異(setPercentstart同時起作用)。桐臊。胎撤。

所以我們得繼續(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ù)refreshListenerloadListener是否為null來進(jìn)行判斷

@Override
public boolean isRefreshAble() {
    return refreshListener != null;
}

@Override
public boolean isLoadAble() {
    return loadListener != null;
}

那么何時響應(yīng)onRefreshrefreshCancel呢?必然是在computeScrollsetRefreshing之中做文章

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的支持

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市褒纲,隨后出現(xiàn)的幾起案子准夷,更是在濱河造成了極大的恐慌,老刑警劉巖莺掠,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衫嵌,死亡現(xiàn)場離奇詭異,居然都是意外死亡彻秆,警方通過查閱死者的電腦和手機(jī)楔绞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門结闸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人酒朵,你說我怎么就攤上這事桦锄。” “怎么了蔫耽?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵结耀,是天一觀的道長。 經(jīng)常有香客問我匙铡,道長饼记,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任慰枕,我火速辦了婚禮,結(jié)果婚禮上即纲,老公的妹妹穿的比我還像新娘具帮。我一直安慰自己,他們只是感情好低斋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布蜂厅。 她就那樣靜靜地躺著,像睡著了一般膊畴。 火紅的嫁衣襯著肌膚如雪掘猿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天唇跨,我揣著相機(jī)與錄音稠通,去河邊找鬼。 笑死买猖,一個胖子當(dāng)著我的面吹牛改橘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播玉控,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼飞主,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了高诺?” 一聲冷哼從身側(cè)響起碌识,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虱而,沒想到半個月后筏餐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡薛窥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年胖烛,在試婚紗的時候發(fā)現(xiàn)自己被綠了眼姐。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡佩番,死狀恐怖众旗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情趟畏,我是刑警寧澤贡歧,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站赋秀,受9級特大地震影響利朵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜猎莲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一绍弟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧著洼,春花似錦樟遣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至液荸,卻和暖如春瞻佛,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背娇钱。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工伤柄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人忍弛。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓响迂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親细疚。 傳聞我的和親對象是個殘疾皇子蔗彤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

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