自定义 View(二)自己动手实现下拉刷新、上拉加载功能

博主声明:

转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。

本文首发于此   博主:威威喵  |  博客主页:https://blog.csdn.net/smile_running

 

系列文章:

自定义 View(一)仿 QQ 列表 Item 侧拉删除功能

自定义 View(二)自己动手实现下拉刷新、上拉加载功能

自定义 View(三)仿 DrawerLayout 实现侧拉功能

   继续我上篇文章的内容:Android进阶(一)为ListView的每项Item添加侧拉删除菜单按钮功能,这篇我将给ListView加上上拉刷新、下拉加载的动画效果。

    其实,这篇内容和上篇内容用到的原理、逻辑、思路及实现等基本都类似。所谓一通百通啊,真的是这样,你只要掌握自定义View的一些套路,其实也不是很难嘛。

    主要解决问题(ListView 与下拉刷新、上拉加载的滑动冲突)

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

  • 效果图

  • 实现思路和代码

    那么看这样实现,如果你没做过的话,是不是觉得这个很复杂呢?其实并不然。首先,依然是我们的布局,布局分上、中、下三部分。上为上拉刷新内容、中为ListView、下为下拉加载内容。只要你清楚了这样的布局,那么实现起来轻轻松松啊,有没有?

布局文件:

    看一下我们的布局文件:

    

        

            

            

            
        

        

        

            

                

                
            

        
    

    布局里的内容元素我就不做多的说明了,也没什么好说明的。我们看最外层这个控件,是我自定义的继承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,Android)