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

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

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

这里我们先不着急写这个ViewGroup,先把Header和Footer的抽象父类定义出来,这样以后就可以继承父类随时实现各种酷炫的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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容