美文网首页android技术专栏安卓开发相关Android知识
简单造轮子系列 - 自定义支持下拉刷新上拉加载的RefreshL

简单造轮子系列 - 自定义支持下拉刷新上拉加载的RefreshL

作者: 諸星団 | 来源:发表于2016-09-02 16:11 被阅读919次

    常听各种大神说,不能只限于会使用别人的框架,一定要会自己造轮子。做Android开发以来,自己也写了一些自定义的View,以前太懒没写过博客,最近觉得还是记下来吧,于是准备写这么一个系列,虽然完全不知道要写到什么时候。

    说到下拉刷新上拉加载的原理嘛,其实很简单,自定义一个ViewGroup,加入3个子View,一个Header,一个Target,一个Footer,其中Header和Footer隐藏起来。之后监听Target的滑动状态,一旦Target向下拉拉不动就显示Header,上拉拉不动就显示Footer,完成版效果图如下:

    public abstract class QLoadView extends ViewGroup{
            public static final int STATE_REFRESH = 0;
            public static final int STATE_NORMAL = 1;
            public static final int STATE_PULLING = 2;
            public static final int STATE_COMPLETE = 3;
            
            public abstract void setNormal();
            public abstract void setPulling();
            public abstract void setRefreshing();
            public abstract void setComplete();
    

    这里在父类中定义4个状态及其4个改变状态的方法,代表如下:

    状态 说明
    Normal
    Pulling
    Refreshing
    Complete

    一般来说,普通的的Header、Footer应该是逃不出这4种状态的。

    LoadView搞定后,开始正儿八经的写RefreshLayout,这里我选择直接继承FrameLayout,定义几个成员变量并在onFinishInflate时为其赋值。

    private QLoadView mHeaderView;
        private QLoadView mFooterView;
        private View mTarget;
        
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            if (getChildCount() > 1) {
                throw new IllegalStateException("QRefreshLayout can 
                only have one child");
            }
            mTarget = getChildAt(0);
            if (mHeaderView == null) {
                setHeaderView(new HeaderView(getContext()));
            }
            if (mFooterView == null) {
                setFooterView(new FooterView(getContext()));
            }
            
        public void setHeaderView(QLoadView view) {
            if (view == mHeaderView) return;
            if (mHeaderView != null) removeView(mHeaderView);
            mHeaderView = view;
            LayoutParams headerParams = new 
            LayoutParams(LayoutParams.MATCH_PARENT, 0);
            addView(mHeaderView, headerParams);
        }
    
        public void setFooterView(QLoadView view) {
            if (view == mFooterView) return;
            if (mFooterView != null) removeView(mFooterView);
            mFooterView = view;
            LayoutParams footerParams = new 
            LayoutParams(LayoutParams.MATCH_PARENT, 0);
            footerParams.gravity = Gravity.BOTTOM;
            addView(mFooterView, footerParams);
        }
    

    先将Header和Footer的高度都设为0,这样就只有Target会显示,然后监听手指的滑动距离,动态的调整Header或者Footer的高度另其显示,这里我选择将Touch监听放到dispatchTouchEvent中去做,这样可以很方便的在Touch事件传递给Target之前控制Header和Footer。

    dispatchTouchEvent中,最重要的逻辑就是处理好ACTION_MOVE,首先通过move的距离判断出手指是在向上滑还是向下滑并通过canTargetScrollUp()canTargetScrollDown()来判断Target有没有滑到头,最终计算出要触发哪种模式(上拉刷新还是下拉加载)

    case MotionEvent.ACTION_MOVE: {
                    float currY = event.getY();
                    float dy = currY - mTouchY;
                    mTouchY = currY;
                    if (dy > 0 && !canTargetScrollUp() && !mAction) {
                        mMode = MODE_REFRESH;
                    } else if (dy < 0 && mLoadMoreEnable &&
                     !canTargetScrollDown() && !mAction) {
                        mMode = MODE_LOADMORE;
                    }
                    handleScroll(dy);
                    break;
                }
    

    canTargetScrollUp()中使用ViewCompat.canScrollVertically(View v, int direction)方法来判断Target是否还能继续滚动。

    /** * Check if this view can be scrolled vertically in a certain direction. * @param v The View against which to invoke the method. * @param direction Negative to check scrolling up, positive to check  * scrolling down. * @return true if this view can be scrolled in the specified direction,  * false otherwise. */
    

    观察这个方法的说明可以发现,第二个参数传负值可以检测view上方是否可以滚动,传正值可以检测view下方是否可以滚动。这里比较有意思,这个上方滚动其实正好相当于下拉,下方滚动相当于上拉,请看图:

    灵魂画手再现!

    跑题了....接着回来说,判断完是上拉下拉之后就进入到主逻辑,这里以下拉为例,根据手指移动的y坐标距离来设置Header的高度,同时设置Target的y坐标偏移量,这样就是一种随着手指不断的往下拉,Header就被拉出来的感觉了。

    private void handleScroll(int dy){
            LayoutParams params = (LayoutParams) mHeaderView.getLayoutParams();
            params.height += dy;
            int dragIndex = Math.exp(-(params.height / 400f));
            if (mDragIndex < 0) mDragIndex = 0;
            params.height += dy * mDragIndex;
            if (params.height > mRefreshHeight) {
                syncLoadViewState(mHeaderView, QLoadView.STATE_PULLING);
            }
            mHeaderView.setLayoutParams(params);
            mTarget.setTranslationY(params.height);
        }
    

    这里先通过计算得来的Header新高度计算出一个拖拽系数,然后用手指移动的距离乘以这个系数后再真正的赋给Header的Height,为啥要这么干呢?因为我们要制造一种下拉的阻尼感,越拉越难拉。我们知道,一个负数的N次幂等于这个数正数N次幂的倒数,用负指数的原理计算出这么一个系数,这样随着我们手指移动的距离越长,Header的高度增加的就越来越慢,给人一种越来越拖不动的感觉。

    当Header的高度拖到我们规定的mRefreshHeight时,就将Header的内容切换到Pulling状态,还记得定义的QLoadView父类么,这个时候派上用场了。

    当拉动到规定的mRefreshHeight时,抬起手指触发刷新,这个时候只需要将Header的内容切换到Refreshing状态即可,也可以设置一个Header在Refreshing状态的高度,使用ValueAnimator将Header调整到此高度。如果抬起手指时没有达到mRefreshHeight,把Header的内容切换到Normal状态并使用ValueAnimator将Header高度调整到0再次隐藏起来。

    int state = -1;
        int height = view.getHeight();
        if (height > mRefreshHeight) {
              height = mFinalHeight;
              state = QLoadView.STATE_REFRESH;
        } else if (height < mRefreshHeight) {
              height = 0;
              state = QLoadView.STATE_NORMAL;
        }
        startPullAnime(view, height, null);
        syncLoadViewState(view, state);
    

    下拉刷新的主要逻辑到这就写完了,后续只需要给外部提供一个监听,像这样:

    public interface RefreshHandler {
          void onRefresh(QRefreshLayout refresh);
          void onLoadMore(QRefreshLayout refresh);
      }
    

    并提供一个刷新完成的方法给外部调用

        public void refreshComplete() {
            mRefreshing = false;
            mHeaderView.setComplete();
            startPullAnime(mHeaderView, 0, new AnimeListener(mHeaderView));
        }
    

    刷新完成后将Header内容切换到Complete状态,并使用ValueAnimator将Header高度调整到0隐藏,当动画结束后,再次将Header内容切换到Normal状态。

    上拉加载功能和其原理一样,就不再过多叙述。

    因为我们在RefreshLayout调用的都是QLoadView这个抽象父类的方法,所以我们可以随意的继承此类创建自己喜欢的Header、Footer。

    同时还可以进行简单的扩展,比如设置一个Google下拉风格,下拉的时候只调整Header的高度,并不处理Target的y坐标,分分钟变成SwipeRefreshLayout。只需要加一句

      if (mStyle == STYLE_CLASSIC)
                mTarget.setTranslationY(height);
    

    就这样,我们就实现了一个上拉下拉都有的RefreshLayout。

    demo源码:https://github.com/qstumn/QRefreshLayout

    感谢:https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh

    相关文章

      网友评论

        本文标题:简单造轮子系列 - 自定义支持下拉刷新上拉加载的RefreshL

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