尝试自己造一个上下拉刷新控件

下拉刷新和上拉加载更多,是一种非常常见的用户交互方式,在开发中大家往往会根据自己的项目选择一款合适的优秀开源框架。但说不定哪天需要自己手动实现类似的效果,同时也本着知其然知其所以然的目的,所以很有必要了解一下实现的方式,当然实现的方式也有几种,当前方式还是比较简单的,实现效果:

这里写图片描述

其实功能和效果差不多,就没好看点的图片,布局简单了些,看起来档次稍低。。一步一步来实现。

一丶布局

(上拉刷新下拉加载更多)控件说白了就是一个大的ViewGroup里面包裹着三个子View:上拉刷新的HeaderView,ContentView,下拉加载更多FooterView。当手指滑动,符合刷新/加载时,通过内容滑动的方式让HeaderView/FooterView随着手势移动到可见的区域,在更新数据后View回弹到默认位置。默认情况下不进行任何操作或者滑动时属于ContentView中的RecylerView时,屏幕只有ContentView这块区域可见,而HeaderView和FooterView都是看不见的。

这里写图片描述

实现这种布局,其实也比较简单。子View在屏幕中的位置摆放是由父ViewGroup负责和控制的。只需要在ViewGroup中的onLayout()方法中让ContentView撑满整个屏幕的大小,HeaderView/FooterView分别置于可见区域上下方,具体代码(以1080*1980为例,减去Theme和Bar的高度剩下1710):

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for (int i = 0; i < getChildCount(); i++) {
        View view = getChildAt(i);
        if (view == mHeaderView) {
            view.layout(0, -500,1080,0);
        } else if (view == mFooterView) {
            view.layout(0,1710,1080,1710+500);
        } else {
            view.layout(0,0,1080,1710);
        }
    }
}

二丶滑动

当手势滑动符合刷新或者加载时,需要响应用户的操作让HeaderView或者FooterView随着手势的滑动慢慢的变为可见。一番比较后还是觉得使用scollTo()||scollBy()内容滑动来实现方便简单得多,为了让滑动更加油质感,选择使用基于内容滑动的Scoller。

private float mStartY;

public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mStartY = event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            if (!mScroller.computeScrollOffset()) {
                int scollDis = Math.round(event.getY() - mStartY);
                int scrollY = getScrollY();
                    if (y != 0) {                   
                        /*if (y > 0) {  //手势下滑
                            y = -y;
                        } else {  //手势上滑
                            y = Math.abs(y);
                        }*/
                        y = y> 0 ? -y: Math.abs(y);
                        textUpdate(scrollY);
                        mScroller.startScroll(0, scrollY, 0, y, 0);
                        invalidate();
                    }
                }
            break;
    }
    mStartY = event.getY();
    return true;
}

在触摸屏幕和事件结束时都会更新(起点)Y值,当手势移动时再根据当前Y点坐标(终点),计算出这次手势滑动的距离,根据距离值判断出手势滑动的方向,最终再经过Scoller滑动ViewGroup的整个内容。效果大致如下:

三丶滑动冲突解决

由于ContentView区域是一个RecylerView,RecylerView滑动和ViewGroup的滑动就会造成滑动冲出。RecylerView又是ViewGroup的一个子View,所以使用外部拦截法来解决滑动的冲突。逻辑分析:

Case1:当RecylerView第一个Item完全可见,并且手势下拉时拦截事件ViewGroup自己消费;

这里写图片描述

Case2:当RecycrView滑动到底部最后一个Item完全可见,并且手势上拉时拦截事件ViewGroup自己消费;

这里写图片描述

Case3:其他情况(第一个Item可见但上拉;最后一个Item可见但下拉;第一和最后一个都看不见时,完全属于RecyclerView)这三种情况下,放行事件交给RecyclerView消费;代码如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    mCount = mRecyclerView.getAdapter().getItemCount();
    mLayoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
    int position = mLayoutManager.findFirstCompletelyVisibleItemPosition();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mStartY = ev.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            int y = Math.round(ev.getY() - mStartY);
            if (position == 0 && y > 0) { //case1
                return true;
            } else if (mLayoutManager.findLastCompletelyVisibleItemPosition() == mCount - 1 && y < 0) { //case2
                return true;
            }
            //case3
            return false;
    }
    return false;
}

四丶内容回弹

当松开手时也需要作出反馈,分2种情况
Case1:
滑动超过规定的距离,符合请求数据的条件,先回弹一些距离,请求数据后再回弹初始位置;
这里写图片描述

Case2:
滑动未超过规定的距离,不符合请求数据的条件,直接回弹到初始位置;

这里写图片描述

代码如下:

//回弹操作
private void reBound() {
    int scrollY = getScrollY();
    int scrollDis; //滑动距离
    int absY = Math.abs(scrollY);
    if (scrollY != 0) {
    //判断是否超过200
        if (absY < 200) { //没超过:直接回弹到默认位置
            scrollDis = scrollY > 0 ? 0 - scrollY : absY;
            mScroller.startScroll(0, scrollY, 0, scrollDis, 1000);
            invalidate();
            return;
        } else { //超过200需要加载数据
            scrollDis = scrollY > 0 ? 0 - (scrollY - 100) : absY - 100;
            mScroller.startScroll(0, scrollY, 0, scrollDis, 1000);
            invalidate();
            // 加载/刷新数据
            postDelayed(new Runnable() {
                @Override
                public void run() {
                    int y = getScrollY();
                    if (y > 0) { //上滑
                        mCallBack.upRefresh(mScroller, y);
                    } else { //下滑
                    mCallBack.downLoad(mScroller, y);
                    }
                }
            }, 2000);
        }
    }
}

问题还比较多,主要说明的是实现方式,代码也只贴了关键的一些。大家可以进一步的改善。
github地址:https://github.com/yangjiechina/PullRefreshDemo2

猜你喜欢

转载自blog.csdn.net/sinat_35938012/article/details/78177918
今日推荐