ListView添加下拉刷新、上拉加载,其实很简单。(解决ListView与下拉刷新、上拉加载的滑动冲突)

版权声明:本文是博主原创文章,不得转载本文任何内容。原文地址: https://blog.csdn.net/smile_Running/article/details/81950872

                         【声明】:未经许可,禁止转载!

· 介绍

    继续我上篇文章的内容:一步步实现ListView的Item侧滑删除菜单效果,仿QQ的聊天页面侧滑删除,这篇我将给ListView加上上拉刷新、下拉加载的动画效果。其实,这篇内容和上篇内容用到的原理、逻辑、思路及实现等基本都类似。所谓一通百通啊,真的是这样,你只要掌握自定义View的一些套路,其实也不是很难嘛。

先来看看我实现的效果,首先是上拉刷新的效果:

那么看这样实现,如果你没做过的话,是不是觉得这个很复杂呢?其实并不然。

首先,依然是我们的布局,布局分上、中、下三部分。上为上拉刷新内容、中为ListView、下为下拉加载内容。只要你清楚了这样的布局,那么实现起来轻轻松松啊,有没有?。

看一下我们的布局文件:

    <listview.example.x.slidelistview.RefreshLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="180dp"
            android:background="@android:color/holo_red_dark"
            android:gravity="center_horizontal">

            <ProgressBar
                android:id="@+id/refresh_progress"
                android:layout_width="24dp"
                android:layout_height="24dp"
                android:layout_gravity="bottom|right"
                tools:ignore="RtlHardcoded" />

            <TextView
                android:id="@+id/tv_refresh_state"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="bottom|center_horizontal"
                android:layout_marginBottom="32dp"
                android:layout_marginTop="8dp"
                android:textColor="@android:color/white" />

            <ImageView
                android:id="@+id/iv_refreshing"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_gravity="center_horizontal|bottom"
                android:src="@drawable/ic_flight_black_24dp" />
        </FrameLayout>

        <ListView
            android:id="@+id/lv_contact"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="80dp"
            android:background="@android:color/holo_orange_dark"
            android:gravity="center">

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:orientation="vertical">

                <ProgressBar
                    android:id="@+id/load_progress"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content" />

                <TextView
                    android:id="@+id/tv_load_state"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="正在加载"
                    android:textColor="@android:color/white" />
            </LinearLayout>

        </RelativeLayout>
    </listview.example.x.slidelistview.RefreshLayout>

布局里的内容元素我就不做多的说明了,也没什么好说明的。我们看最外层这个控件,是我自定义的继承FrameLayout的一个RefreshLayout类。为什么用FrameLayout?我在上篇文章已经做了说明了,不清楚的依然可以在上面推荐链接点进去查看。首先,我们将这三个家伙进行布局,当然是从上到下的那种。来看看代码:

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mHeaderView.layout(0, -mHeaderHeight, mHeaderWidth, 0);
        mContentView.layout(0, 0, mContentWidth, mContentHeight);
        mFooterView.layout(0, mContentHeight, mFooterWidth, mContentHeight + mFooterHeight);
    }

这就完成了我从上至下的布局。

既然,我们把它布局到了屏幕上方,显然是看不见的。现在只能通过手指将它滑动下来显示,那么我们在touch事件做滑动处理,来看看代码。

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = x;
                startY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (isTop) {
                    if (firstDownTag == 0) {
                        /**
                         * 如果是第一次的话,因为事件传递原因
                         * onInterceptTouchEvent()执行了 ACTION_DOWN事件
                         * 标记了startY的值(这个值也许非常大,是根据手指按下的y坐标来定的)
                         * 关键是onTouchEvent的ACTION_DOWN无法得到执行,所以 scrollTo(0, disY);将直接移动到startY的位置
                         * 效果就是导致第一次向下拉,瞬间移动了非常多
                         */
                        firstDownTag++;
                    } else {
                        final float dy = y - startY;
                        int disY = (int) (getScrollY() - dy);
                        if (-disY <= 0) {
                            disY = 0;
                        }

                        if (-disY < mHeaderHeight) {
                            scrollTo(0, disY);
                            mRefreshProgress.setVisibility(INVISIBLE);
                            if (-disY < mRefreshHeight) {
                                tvRefreshText.setText("准备起飞");
                                startRefreshIcon();
                            } else {
                                tvRefreshText.setText("加速中");
                                stopRefreshIcon();
                            }
                        }
                    }
                }
                startX = x;
                startY = y;
                break;
            case MotionEvent.ACTION_UP:
                isIntercept = false;
                if (-getScrollY() > mRefreshHeight) {
                    startRefreshing();
                } else {
                    stopRefreshing();
                }
                break;
        }
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final float x = ev.getX();
        final float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isIntercept = false;
                upX = x;
                upY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (isTop) {
                    if (upY - y < 0) {
                        isIntercept = true;
                    } else if (y - upY < 0) {
                        isIntercept = false;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                upY = 0;
                upX = 0;
                break;
        }
        return isIntercept;
    }

    这里有一个大坑我们得爬,就是在RefreshLayout不拦截事件的时候,它默认会分发事件给ListView,导致ListView把touch事件给消费了,所以不拦截的情况下,尽管你怎么往下拉,它始终是拉不出来的。哈哈,那么解决方法就是我们拦截它。但是拦截总是有条件的,这个条件有两点:

1、ListView的子项在第一个,也就是到达最顶部。

2、如果在ListView到达顶部前提下,手指还继续往上滑动,那么就是下拉刷新的动作了,在此时拦截它。

   上面代码就是做了这两件事情,还有就是滑动动画。当然,这得在我们RefreshLayout中实现对ListView的滑动监听的接口,判断是否处于顶部和底部:​​​​​​。还有一个就是我们的飞机动画了,这比较简单了。

既然说完了上拉刷新,下面我们来看看下拉加载动画吧。

其实,上拉加载只是和我们的下拉刷新方向相反的。既然我们已经实现了下拉刷新,那么上拉加载还不是手到擒来嘛。因为我们前面已经处理过了事件冲突,所以可以一路向前,通畅无阻。

我们看一下关键代码,最主要的还是我们的touch事件的代码,添加上拉加载的逻辑代码,其他都非常简单了:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = x;
                startY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (isTop) {
                    if (firstDownTag == 0) {
                        /**
                         * 如果是第一次的话,因为事件传递原因
                         * onInterceptTouchEvent()执行了 ACTION_DOWN事件
                         * 标记了startY的值(这个值也许非常大,是根据手指按下的y坐标来定的)
                         * 关键是onTouchEvent的ACTION_DOWN无法得到执行,所以 scrollTo(0, disY);将直接移动到startY的位置
                         * 效果就是导致第一次向下拉,瞬间移动了非常多
                         */
                        firstDownTag++;
                    } else {
                        final float dy = y - startY;
                        int disY = (int) (getScrollY() - dy);
                        if (-disY <= 0) {
                            disY = 0;
                        }

                        if (-disY < mHeaderHeight) {
                            scrollTo(0, disY);
                            mRefreshProgress.setVisibility(INVISIBLE);
                            if (-disY < mRefreshHeight) {
                                tvRefreshText.setText("准备起飞");
                                startRefreshIcon();
                            } else {
                                tvRefreshText.setText("加速中");
                                stopRefreshIcon();
                            }
                        }
                    }
                } else if (isBottom) {/** 在ListView底部,继续上拉 **/
                    final float dy = y - startY;
                    int disY = (int) (getScrollY() - dy);
                    if (disY < 0) {
                        disY = 0;
                        ivLoadingIcon.setVisibility(VISIBLE);
                        mLoadingProgress.setVisibility(INVISIBLE);
                    } else if (disY >= mLoadingHeight) {
                        disY = mLoadingHeight + 5;
                    }
                    scrollTo(getScrollX(), disY);

//                    if (dy < 0) {
//                        startLoadingIcon();
//                    } else {
//                        stopLoadingIcon();
//                    }
                }
                startX = x;
                startY = y;
                break;
            case MotionEvent.ACTION_UP:
                isIntercept = false;
                if (isTop) {
                    if (-getScrollY() > mRefreshHeight) {
                        startRefreshing();
                    } else {
                        stopRefreshing();
                    }
                } else if (isBottom) {
                    if (getScrollY() > mLoadingHeight) {
                        startLoading();
                    } else {
                        stopLoading();
                    }
                }
                break;
        }
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final float x = ev.getX();
        final float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isIntercept = false;
                upX = downX = x;
                upY = downY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (isTop) {
                    /** 下拉刷新拦截 **/
                    if (upY - y < 0) {
                        isIntercept = true;
                    } else if (y - upY < 0) {
                        isIntercept = false;
                    }
                } else if (isBottom) {
                    /** 上拉加载拦截 **/
                    if (y - downY < 0) {
                        isIntercept = true;
                    } else if (y - downY > 0) {
                        isIntercept = false;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                downX = upY = 0;
                downX = upX = 0;
                break;
        }
        return isIntercept;
    }

飞机转头和动画代码:

  private void stopRefreshing() {
        mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY());
        /**
         * ListView子项移动到第一个
         */
        mListView.setSelection(0);
        invalidate();
    }

    private void startRefreshing() {
        mScroller.startScroll(getScrollX(), getScrollY(), 0, -mRefreshHeight - getScrollY());
        tvRefreshText.setText("起飞咯~");
        mRefreshProgress.setVisibility(VISIBLE);
        startIconAnimation();
        invalidate();
        /**
         * 模拟刷新完成,延迟关闭
         */
        handler.postDelayed(() -> stopRefreshing(), 2000);
    }

    private void startLoading() {
        mScroller.startScroll(getScrollX(), getScrollY(), 0, mFooterHeight - getScrollY());
        ivLoadingIcon.setVisibility(INVISIBLE);
        mLoadingProgress.setVisibility(VISIBLE);
        invalidate();
        handler.postDelayed(() -> stopLoading(), 1500);
    }

    private void stopLoading() {
        mScroller.startScroll(getScrollX(), getScrollY(), 0, -getScrollY(),1500);
        ivLoadingIcon.setVisibility(VISIBLE);
        mLoadingProgress.setVisibility(INVISIBLE);
        ivLoadingIcon.setPivotX(ivLoadingIcon.getWidth() / 2);
        ivLoadingIcon.setPivotY(ivLoadingIcon.getHeight() / 2);
        ivLoadingIcon.setRotation(180);
        invalidate();
    }

    private void startIconAnimation() {
        TranslateAnimation animation = new TranslateAnimation(0, 0,
                getScaleY(), -mRefreshHeight);
        animation.setFillAfter(false);
        animation.setDuration(2000);
        ivRefreshIcon.startAnimation(animation);
    }

    private void startRefreshIcon() {
        ivRefreshIcon.setPivotX(ivRefreshIcon.getWidth() / 2);
        ivRefreshIcon.setPivotY(ivRefreshIcon.getHeight() / 2);
        ivRefreshIcon.setRotation(180);
    }

    private void stopRefreshIcon() {
        ivRefreshIcon.setPivotX(ivRefreshIcon.getWidth() / 2);
        ivRefreshIcon.setPivotY(ivRefreshIcon.getHeight() / 2);
        ivRefreshIcon.setRotation(360);
    }

那么,我们整个下拉刷新、上拉加载的套餐:

博文续篇:

为之前的自定义View添加DrawerLayout(侧拉抽屉),为自定义View系列画上完美句号。

©版权所有:https://blog.csdn.net/smile_Running/article/details/81950872

猜你喜欢

转载自blog.csdn.net/smile_Running/article/details/81950872