美文网首页
Android 简单易上手的下拉刷新控件

Android 简单易上手的下拉刷新控件

作者: 6FingerGodSword | 来源:发表于2020-08-21 16:35 被阅读0次

    背景:列表控件在Android App开发中用到的场景很多。在以前我们用ListView,GradView,现在应该大多数开发者都已经在选择使用RecyclerView了,谷歌给我们提供了这些方便的列表控件,我们可以很容易的使用它们。但是在实际的场景中,我们可能还想要更多的能力,比如最常见的列表下拉刷新,上拉加载。上拉刷新和下拉加载应该是列表的标配吧,基本上有列表的地方都要具体这个能力。虽然刷新这个功能已经有各种各样的第三方框架可以选择,但是毕竟不是自己的嘛,今天我们就来实现一个自己的下拉刷新控件,多动手才能更好的理解。

    效果图:

    效果图.gif

    原理分析:

    在coding之前,我们先分析一下原理,原理分析出来之后,我们才可以确定实现方案。
    先上一张图,来个直观的认识:


    布局样式.png

    在列表上面有个刷新头,随着手指向下拉,逐渐把顶部不可见的刷新头拉到屏幕中来,用户能看到刷新的状态变化,达到下拉刷新的目的。

    通过分析,我们确定一种实现方案:我们自定义一个容器,容器里面包含两个部分。
    1. 顶部刷新头。
    2. 列表区域。

    确定好布局容器之后,我们来分析刷新头的几种状态,


    下拉刷新状态.png

    把下拉刷新分为5中状态,通过不同状态间的切换实现下拉刷新能力。
    状态间的流程图如下:


    刷新流程图.png

    整个下拉刷新的流程就如图中所示。

    流程清楚了之后,接下来就是编写代码实现了。

    代码实现:

    /**
     * @author luowang8
     * @date 2020-08-21 10:54
     * @desc 下拉刷新控件
     */
    public class PullRefreshView extends LinearLayout {
        
        
        /**
         * 头部tag
         */
        public static final String HEADER_TAG = "HEADER_TAG";
        
        /**
         * 列表tag
         */
        public static final String LIST_TAG   = "LIST_TAG";
        
        /**
         * tag
         */
        private static final String TAG = "PullRefreshView";
        
        /**
         * 默认初始状态
         */
        private @State
        int mState = State.INIT;
        
        /**
         * 是否被拖拽
         */
        private boolean mIsDragging = false;
        
        /**
         * 上下文
         */
        private Context mContext;
        
        
        /**
         * RecyclerView
         */
        private RecyclerView mRecyclerView;
        
        /**
         * 顶部刷新头
         */
        private View mHeaderView;
        
        /**
         * 初始Y的坐标
         */
        private int mInitMotionY;
        
        /**
         * 上一次Y的坐标
         */
        private int mLastMotionY;
        
        /**
         * 手指触发滑动的临界距离
         */
        private int mSlopTouch;
        
        /**
         * 触发刷新的临界值
         */
        private int mRefreshHeight = 200;
        
        /**
         * 滑动时长
         */
        private int mDuring = 300;
        
        /**
         * 用户刷新监听器
         */
        private OnRefreshListener mOnRefreshListener;
        
        /**
         * 刷新文字提示
         */
        private TextView mRefreshTip;
        
        /**
         * 是否可拖拽, 因为在刷新头自由滑动和刷新状态的时候,
         * 我们应该保持界面不被破坏
         */
        private boolean mIsCanDrag = true;
        
        /**
         * 头部布局
         */
        private LayoutParams mHeaderLayoutParams;
        
        /**
         * 列表布局
         */
        private LayoutParams mListLayoutParams;
        
        /**
         * 属性动画
         */
        private ValueAnimator mValueAnimator;
        
        
        
        /////////////////////// 分割 ///////////////////////
        
        
        /**
         * @param context
         */
        public PullRefreshView(Context context) {
            this(context, null);
        }
        
        /**
         * @param context
         * @param attrs
         */
        public PullRefreshView(Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
        
        /**
         * @param context
         * @param attrs
         * @param defStyleAttr
         */
        public PullRefreshView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            
            mContext = context;
            
            initView();
        }
        
        public RecyclerView getRecyclerView() {
            return mRecyclerView;
        }
        
        /**
         * 设置RecyclerView
         *
         * @param recyclerView
         */
        public void addRecyclerView(RecyclerView recyclerView) {
            
            if (recyclerView == null) {
                return;
            }
            
            View view = findViewWithTag(LIST_TAG);
            if (view != null) {
                removeView(view);
            }
            
            this.mRecyclerView = recyclerView;
            this.mRecyclerView.setTag(LIST_TAG);
            addView(recyclerView, mListLayoutParams);
        }
        
        /**
         * 设置自定义刷新头部
         * @param headerView
         */
        public void addHeaderView(View headerView) {
            
            if (headerView == null) {
                return;
            }
            
            View view = findViewWithTag(HEADER_TAG);
            if (view != null) {
                removeView(view);
            }
            
            this.mHeaderView = headerView;
            this.mHeaderView.setTag(HEADER_TAG);
            addView(mHeaderView, mHeaderLayoutParams);
        }
        
        /**
         * @param onRefreshListener
         */
        public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
            mOnRefreshListener = onRefreshListener;
        }
        
        /**
         * 初始化View
         */
        private void initView() {
            
            setOrientation(LinearLayout.VERTICAL);
            
            Context context = getContext();
            /** 1、添加刷新头Header */
            mHeaderView = LayoutInflater.from(context).inflate(R.layout.layout_refresh_header, null);
            mHeaderView.setTag(HEADER_TAG);
            mRefreshTip = mHeaderView.findViewById(R.id.content);
            mHeaderLayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
                    DensityUtil.dip2px(mContext, 500)
            );
            this.addView(mHeaderView, mHeaderLayoutParams);
            
            /** 2、添加内容RecyclerView */
            mRecyclerView = new RecyclerView(context);
            mRecyclerView.setTag(LIST_TAG);
            mListLayoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            this.addView(mRecyclerView, mListLayoutParams);
            
            /** 3、一开始的时候要让Header看不见,设置向上的负paddingTop */
            setPadding(0, -DensityUtil.dip2px(mContext, 500), 0, 0);
            
            ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
            mSlopTouch = viewConfiguration.getScaledTouchSlop();
            
            setState(State.INIT);
            
        }
        
        /**
         * 设置状态,每个状态下,做不同的事情
         *
         * @param state 状态
         */
        private void setState(@State int state) {
            
            switch (state) {
                case State.INIT:
                    initState();
                    break;
                
                case State.DRAGGING:
                    dragState();
                    break;
                
                case State.READY:
                    readyState();
                    break;
                
                case State.REFRESHING:
                    refreshState();
                    break;
                
                case State.FLING:
                    flingState();
                    break;
                
                default:
                    break;
            }
            
            mState = state;
        }
        
        /**
         * 处理初始化状态方法
         */
        private void initState() {
            
            // 只有在初始状态时,恢复成可拖拽
            mIsCanDrag = true;
            mIsDragging = false;
            mRefreshTip.setText("下拉刷新");
        }
        
        /**
         * 处理拖拽时方法
         */
        private void dragState() {
            mIsDragging = true;
        }
        
        /**
         * 拖拽距离超过header高度时,如何处理
         */
        private void readyState() {
            mRefreshTip.setText("松手刷新");
        }
        
        /**
         * 用户刷新时,如何处理
         */
        private void refreshState() {
            if (mOnRefreshListener != null) {
                mOnRefreshListener.onRefresh();
            }
            
            mIsCanDrag = false;
            mRefreshTip.setText("正在刷新,请稍后...");
        }
        
        /**
         * 自由滚动时,如何处理
         */
        private void flingState() {
            mIsDragging = false;
            mIsCanDrag = false;
            
            /** 自由滚动状态可以从两个状态进入:
             *  1、READY状态。
             *  2、其他状态。
             *
             *  !滑动均需要平滑滑动
             *  */
            if (mState == State.READY) {
                
                Log.e(TAG, "flingState: 从Ready状态开始自由滑动");
                // 从准备状态进入,刷新头滑到 200 的位置
                
                smoothScroll(getScrollY(), -mRefreshHeight);
            }
            else {
                
                Log.e(TAG, "flingState: 松手后,从其他状态开始自由滑动");
                // 从刷新状态进入,刷新头直接回到最初默认的位置
                // 即: 滑出界面,ScrollY 变成 0
                smoothScroll(getScrollY(), 0);
            }
            
        }
        
        /**
         *  光滑滚动
         * @param startPos 开始位置
         * @param targetPos 结束位置
         */
        private void smoothScroll(int startPos, final int targetPos) {
            
            // 如果有动画正在播放,先停止
            if (mValueAnimator != null && mValueAnimator.isRunning()) {
                mValueAnimator.cancel();
                mValueAnimator.end();
                mValueAnimator = null;
            }
            
            // 然后开启动画
            mValueAnimator = ValueAnimator.ofInt(getScrollY(), targetPos);
            mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    int value = (int) valueAnimator.getAnimatedValue();
                    scrollTo(0, value);
                    
                    if (getScrollY() == targetPos) {
                        if (targetPos != 0) {
                            setState(State.REFRESHING);
                        }
                        else {
                            setState(State.INIT);
                        }
                    }
                }
            });
            
            mValueAnimator.setDuration(mDuring);
            mValueAnimator.start();
        }
        
        /**
         * 是否准备好触发下拉的状态了
         */
        private boolean isReadyToPull() {
            
            if (mRecyclerView == null) {
                return false;
            }
            
            LinearLayoutManager manager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
            
            if (manager == null) {
                return false;
            }
            
            if (mRecyclerView != null && mRecyclerView.getAdapter() != null) {
                View child  = mRecyclerView.getChildAt(0);
                int  height = child.getHeight();
                if (height > mRecyclerView.getHeight()) {
                    return child.getTop() == 0 && manager.findFirstVisibleItemPosition() == 0;
                }
                else {
                    return manager.findFirstCompletelyVisibleItemPosition() == 0;
                }
            }
            
            return false;
        }
        
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            
            int action = ev.getAction();
            
            Log.e(TAG, "onInterceptTouchEvent: action = " + action);
            
            if (!mIsCanDrag) {
                return true;
            }
            
            if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                mIsDragging = false;
                return false;
            }
            
            if (mIsDragging && action == MotionEvent.ACTION_MOVE) {
                return true;
            }
            
            switch (action) {
                case MotionEvent.ACTION_MOVE:
                    int diff = (int) (ev.getY() - mLastMotionY);
                    if (Math.abs(diff) > mSlopTouch && diff > 1 && isReadyToPull()) {
                        mLastMotionY = (int) ev.getY();
                        mIsDragging = true;
                    }
                    break;
                
                case MotionEvent.ACTION_DOWN:
                    if (isReadyToPull()) {
                        setState(State.INIT);
                        mInitMotionY = (int) ev.getY();
                        mLastMotionY = (int) ev.getY();
                    }
                    break;
                
                default:
                    break;
            }
            
            return mIsDragging;
        }
        
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            
            int action = event.getAction();
            
            Log.e(TAG, "onTouchEvent: action = " + action);
            
            if (!mIsCanDrag) {
                return false;
            }
            
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    if (isReadyToPull()) {
                        setState(State.INIT);
                        mInitMotionY = (int) event.getY();
                        mLastMotionY = (int) event.getY();
                    }
                    break;
                
                case MotionEvent.ACTION_MOVE:
                    
                    if (mIsDragging) {
                        mLastMotionY = (int) event.getY();
                        setState(State.DRAGGING);
                        
                        pullScroll();
                        return true;
                    }
                    
                    break;
                
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mIsDragging = false;
                    setState(State.FLING);
                    break;
                
                default:
                    break;
                
            }
            
            return true;
        }
        
        /**
         * 下拉移动界面,拉出刷新头
         */
        private void pullScroll() {
            /** 滚动值 = 初始值 - 结尾值 */
            int scrollValue = (mInitMotionY - mLastMotionY) / 3;
            
            if (scrollValue > 0) {
                scrollTo(0, 0);
                return;
            }
            
            if (Math.abs(scrollValue) > mRefreshHeight
                    && mState == State.DRAGGING) {
                // 约定:如果偏移量超过 200(这个值,表示是否可以启动刷新的临界值,可任意定),
                // 那么状态变成 State.READY
                Log.e(TAG, "pullScroll: 超过了触发刷新的临界值");
                setState(State.READY);
            }
            
            scrollTo(0, scrollValue);
        }
        
        /**
         * 刷新完成,需要调用方主动发起,才能完成将刷新头收起
         */
        public void refreshComplete() {
            mRefreshTip.setText("刷新完成!");
            setState(State.FLING);
        }
        
        @IntDef({
                        State.INIT
                        , State.DRAGGING
                        , State.READY
                        , State.REFRESHING
                        , State.FLING,
                })
        @Retention(RetentionPolicy.SOURCE)
        public @interface State {
            
            /**
             * 初始状态
             */
            int INIT = 1;
            
            /**
             * 手指拖拽状态
             */
            int DRAGGING = 2;
            
            /**
             * 就绪状态,松开手指后,可以刷新
             */
            int READY = 3;
            
            /**
             * 刷新状态,这个状态下,用户用于发起刷新请求
             */
            int REFRESHING = 4;
            
            /**
             * 松开手指,顶部自然回弹的状态,有两种表现
             * 1、手指释放时的高度大于刷新头的高度。
             * 2、手指释放时的高度小于刷新头的高度。
             */
            int FLING = 5;
        }
        
        /**
         * 用户刷新状态的操作
         */
        public interface OnRefreshListener {
            void onRefresh();
        }
        
    }
    

    实现的逻辑并不复杂,新手都能看懂,先理解了整个流程,代码就是水到渠成的事。
    思想第一,最后代码。

    完整DEMO直通车:https://github.com/wwluo14/PullToRefresh

    相关文章

      网友评论

          本文标题:Android 简单易上手的下拉刷新控件

          本文链接:https://www.haomeiwen.com/subject/qolejktx.html