上拉加載更多,下拉刷新的彈性ListView的實現

本文主要的是介紹如何實現彈性的listview,以及上拉和下拉功能的實現,其實對一般的View也是適用的,稍微修改一下就可以啦。裏面涉及一些對事件分發的處理,有興趣的可以看一下這個鏈接,http://blog.csdn.net/newhope1106/article/details/53363208
源碼地址:https://github.com/newhope1106/flexibleListView有興趣可以試着用一下。
效果圖:

1.使用介紹
(1)首先在xml中定義
<cn.appleye.flexiblelistview.FlexibleListView
        android:id="@+id/flexible_list_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
(2)在代碼中實現回調就可以實現上拉和下拉功能
       mFlexibleListView = (FlexibleListView) findViewById(R.id.flexible_list_view);
       mFlexibleListView.setOnPullListener(new FlexibleListView.OnPullListener(){
            @Override
            public void onPullDown() {
                //下拉刷新
            }

            @Override
            public void onPullUp() {
                //上拉加載更多
            }
        });
2.具體實現
拋開代碼細節,要實現彈性效果和上拉以及下拉功能需要了解以下幾點
(1)什麼是彈性效果?列表滑到底部或者頂部之後,還可以繼續滑動一定距離,然後再慢慢的恢復到底部或者頂部,恢復的過程有一個彈性的效果。
(2)什麼時候觸發?上面可以看到,滑到底部或者頂部之後開始觸發
(3)滑動多少距離開始恢復?定義好一個距離,合適就好
(4)恢復的過程的彈性效果怎麼實現?網上都有很多彈性公式
(5)什麼時候調用上拉或下拉回調?當上拉或下拉到一定距離手指離開開始調用
下面看一下具體代碼怎麼實現的。
/**
 * 彈性ListView,實現了上拉和下拉功能
 * @author newhope1106 2016-11-02
 */
public class FlexibleListView extends ListView implements OnTouchListener{
    /**初始可拉動Y軸方向距離*/
    private static final int MAX_Y_OVER_SCROLL_DISTANCE = 100;

    private Context mContext;

    /**實際可上下拉動Y軸上的距離*/
    private int mMaxYOverScrollDistance;

    private float mStartY = -1;
    /**開始計算的時候,第一個或者最後一個item是否可見的*/
    private boolean mCalcOnItemVisible = false;
    /**是否開始計算*/
    private boolean mStartCalc = false;

    /**用戶自定義的OnTouchListener類*/
    private OnTouchListener mTouchListener;

    /**上拉和下拉監聽事件*/
    private OnPullListener mPullListener;

    private int mScrollY = 0;
    private int mLastMotionY = 0;
    private int mDeltaY = 0;
    /**是否在進行動畫*/
    private boolean mIsAnimationRunning = false;
    /**手指是否離開屏幕*/
    private boolean mIsActionUp = false;

    public FlexibleListView(Context context){
        super(context);
        mContext = context;
        super.setOnTouchListener(this);
        initBounceListView();
    }

    public FlexibleListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        super.setOnTouchListener(this);
        initBounceListView();
    }

    public FlexibleListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mContext = context;
        initBounceListView();
    }

    private void initBounceListView(){
        final DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
        final float density = metrics.density;
        mMaxYOverScrollDistance = (int) (density * MAX_Y_OVER_SCROLL_DISTANCE);
    }

    /**
     * 覆蓋父類的方法,設置OnTouchListener監聽對象
     * @param listener 用戶自定義的OnTouchListener監聽對象
     * */
    public void setOnTouchListener(OnTouchListener listener) {
        mTouchListener = listener;
    }

    /**
     * 設置上拉和下拉監聽對象
     * @param listener 上拉和下拉監聽對象
     * */
    public void setOnPullListener(OnPullListener listener){
        mPullListener = listener;
    }

    public void scrollTo(int x, int y) {
        super.scrollTo(x, y);

        mScrollY = y;
    }

    /**
     * 在滑動的過程中onTouch的ACTION_DOWN事件可能丟失,在這裏進行初始值設置
     * */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mIsActionUp = false;
                resetStatus();
                if(getFirstVisiblePosition() == 0 || (getLastVisiblePosition() == getAdapter().getCount()-1)) {
                    mStartY = event.getY();
                    mStartCalc = true;
                    mCalcOnItemVisible = true;
                }else{
                    mStartCalc = false;
                    mCalcOnItemVisible = false;
                }

                mLastMotionY = (int)event.getY();
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        /*用戶自定義的觸摸監聽對象消費了事件,則不執行下面的上拉和下拉功能*/
        if(mTouchListener!=null && mTouchListener.onTouch(v, event)) {
            return true;
        }

        /*在做動畫的時候禁止滑動列表*/
        if(mIsAnimationRunning) {
            return true;//需要消費掉事件,否者會出現連續很快下拉或上拉無法回到初始位置的情況
        }

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:{
                mIsActionUp = false;
                resetStatus();
                if(getFirstVisiblePosition() == 0 || (getLastVisiblePosition() == getAdapter().getCount()-1)) {
                    mStartY = event.getY();
                    mStartCalc = true;
                    mCalcOnItemVisible = true;
                }else{
                    mStartCalc = false;
                    mCalcOnItemVisible = false;
                }

                mLastMotionY = (int)event.getY();
            }
            case MotionEvent.ACTION_MOVE:{
                if(!mStartCalc && (getFirstVisiblePosition() == 0|| (getLastVisiblePosition() == getAdapter().getCount()-1))) {
                    mStartCalc = true;
                    mCalcOnItemVisible = false;
                    mStartY = event.getY();
                }

                final int y = (int) event.getY();
                mDeltaY = mLastMotionY - y;
                mLastMotionY = y;

                if(Math.abs(mScrollY) >= mMaxYOverScrollDistance) {
                    if(mDeltaY * mScrollY > 0) {
                        mDeltaY = 0;
                    }
                }

                break;
            }
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:{
                mIsActionUp = true;
                float distance = event.getY() - mStartY;
                checkIfNeedRefresh(distance);

                startBoundAnimate();
            }
        }

        return false;
    }

    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
                                  boolean clampedY) {
        if(mDeltaY == 0 || mIsActionUp) {
            return;
        }
        scrollBy(0, mDeltaY/2);
    }
    /**彈性動畫*/
    private void startBoundAnimate() {
        mIsAnimationRunning = true;
        final int scrollY = mScrollY;
        int time = Math.abs(500*scrollY/mMaxYOverScrollDistance);
        ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(time);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
                float fraction = animator.getAnimatedFraction();
                scrollTo(0, scrollY - (int) (scrollY * fraction));

                if((int)fraction == 1) {
                    scrollTo(0, 0);
                    resetStatus();
                    animator.removeUpdateListener(this);
                }
            }
        });
        animator.start();
    }

    private void resetStatus() {
        mIsAnimationRunning = false;
        mStartCalc = false;
        mCalcOnItemVisible = false;
    }

    /**
     * 根據滑動的距離判斷是否需要回調上拉或者下拉事件
     * @param distance 滑動的距離
     * */
    private void checkIfNeedRefresh(float distance) {
        if(distance > 0 && getFirstVisiblePosition() == 0) { //下拉
            View view = getChildAt(0);
            if(view == null) {
                return;
            }

            float realDistance = distance;
            if(!mCalcOnItemVisible) {
                realDistance = realDistance - view.getHeight();//第一個item的高度不計算在內容
            }
            if(realDistance > mMaxYOverScrollDistance) {
                if(mPullListener != null){
                    mPullListener.onPullDown();
                }
            }
        } else if(distance < 0 && getLastVisiblePosition() == getAdapter().getCount()-1) {//上拉
            View view = getChildAt(getChildCount()-1);
            if(view == null) {
                return;
            }

            float realDistance = -distance;
            if(!mCalcOnItemVisible) {
                realDistance = realDistance - view.getHeight();//最後一個item的高度不計算在內容
            }
            if(realDistance > mMaxYOverScrollDistance) {
                if(mPullListener != null){
                    mPullListener.onPullUp();
                }
            }
        }
    }

    public interface OnPullListener{
        /**
         * 下拉
         * */
        void onPullDown();
        /**
         * 上拉
         * */
        void onPullUp();
    }
}
代碼不長,只有200多行,比較簡單,也不涉及資源問題。
首先我們初始化一個最大距離:mMaxYOverScrollDistance,同時控件自己實現OnTouchListener的接口,所有的功能基本都是在onTouch實現的,我們先簡要的描述一下思路。
當手指按下屏幕的時候,檢查此時第一個或者最後一個item是否可見,如果不可見,當滑動手指的時候,檢查此時是否第一個或最後一個item是否可見,在滑動列表時,如果已經超過了listview頂部或底部的位置,通過改變其偏移量mScrollY,讓其可以再在原來的基礎上繼續滑動,但是當滑動到一定距離之後,禁止其改變偏移量,此時不能再繼續滑動了,當手指離開屏幕之後,再彈性回到頂部或底部位置,根據滑動的距離,來判斷是否需要進行下拉或上拉操作。爲什麼,ACTION_DOWN和ACTION_UP中都有這個檢測,主要是爲了在最後計算距離的時候判斷是否需要減去第一個item的高度,當然讀者也可以把它去掉,item高度不大的情況下,不會影響體驗。下面看代碼。
          case MotionEvent.ACTION_DOWN:{
                mIsActionUp = false;
                resetStatus();
                if(getFirstVisiblePosition() == 0 || (getLastVisiblePosition() == getAdapter().getCount()-1)) {
                    mStartY = event.getY();
                    mStartCalc = true;
                    mCalcOnItemVisible = true;
                }else{
                    mStartCalc = false;
                    mCalcOnItemVisible = false;
                }

                mLastMotionY = (int)event.getY();
            }
在ACTION_DOWN操作的時候,通過resetStatus(),初始化狀態,然後檢查第一個item或者最後一個item是否顯示,mStartCalc表示開始計算距離,mCalcOnItemVisible表示是否第一個item或者最後一個item可見的,如果是mStartCalc置爲true,mCalcOnItemVisible置爲true,同時開始記錄當前位置座標。
           case MotionEvent.ACTION_MOVE:{
                if(!mStartCalc && (getFirstVisiblePosition() == 0|| (getLastVisiblePosition() == getAdapter().getCount()-1))) {
                    mStartCalc = true;
                    mCalcOnItemVisible = false;
                    mStartY = event.getY();
                }

                final int y = (int) event.getY();
                //獲取滑動的偏移量
                mDeltaY = mLastMotionY - y;
                mLastMotionY = y;

                if(Math.abs(mScrollY) >= mMaxYOverScrollDistance) {
                    if(mDeltaY * mScrollY > 0) {
                        mDeltaY = 0;
                    }
                }

                break;
            }
如果在ACTION_DOWN中沒有開始計算,那麼在ACTION_MOVE中判斷是否第一個或最後一個item可見,如果是,則將mStartCalc置爲true,mCalcOnItemVisible置爲false。將本次的位置和上次的y周位置進行比較,獲取偏移量。在滑動的過程中,都會調用onOverScrolled接口,然後調用scrollBy(實質上是調用scrollTo)接口,從而實現列表滑動。
   protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
                                  boolean clampedY) {
        //滑動偏移量等於0或者手指離開屏幕都不在滑動列表
        if(mDeltaY == 0 || mIsActionUp) {
            return;
        }
        scrollBy(0, mDeltaY/2);
    }
上述的ACTION_MOVE中會判斷當前listview的偏移量(mScrollY)是否超過最大距離,否則將滑動的偏移量(mDeltaY)置爲0,不讓其在onOverScrolled中滑動。上述的mDeltaY/2,作用是不讓其滑動太快,自然一些。
            case MotionEvent.ACTION_UP:{
                mIsActionUp = true;
                float distance = event.getY() - mStartY;
                checkIfNeedRefresh(distance);

                startBoundAnimate();
            }
當手指離開屏幕的時候,會調用ACTION_UP,此時將mIsActionUp置爲true,同時計算當前位置的座標和初始計算的位置座標,然後得出滑動的距離(往返滑動的情況不計算,只計算初始和終止位置),checkIfNeedRefresh用於判斷是否需要上拉或者下拉操作,根據distance的正負可以知道是上滑還是下滑,如果有必要,減去第一個或最後一個item的高度,得到listview實際滑動的距離,然後和最大距離進行比較,來判斷是否需要上拉加載更多,下拉刷新。
最後通過一個動畫startBoundAnimate實現彈性恢復的效果,動畫過程中不允許其滑動。
       /*在做動畫的時候禁止滑動列表*/
       if(mIsAnimationRunning) {
            return true;//需要消費掉事件,否者會出現連續很快下拉或上拉無法回到初始位置的情況
       }
一下有幾個注意點,onTouch一般情況下返回false,表示不消費事件,不能影響ListView的正常滑動。上拉或者下拉的時候,這裏並沒有做Loading效果,讀者可以自行添加一個footerView或者HeaderView來實現。
這裏都是在View的接口裏面實現的,因此實際上不限於ListView,其他的繼承自View的控件,都可以採用這種方法,如果只想用彈性效果,那麼也沒有必要實現上拉和下拉的效果,直接在xml中定義即可。
還有一點需要注意的是,有時滑動太快,會把ACTION_DOWN事件給忽略掉,因此需要在onInterceptTouchEvent做ACTION_DOWN事件的處理,可以把OnTouch方法中的ACTION_DOWN去掉。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章