美文网首页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