上拉加载更多,下拉刷新的弹性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去掉。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章