博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
SwipeRefreshLayout,用最少的代码定制最美的上下拉刷新样式
阅读量:5995 次
发布时间:2019-06-20

本文共 33590 字,大约阅读时间需要 111 分钟。

下拉刷新框架其实有很多,而且质量都比较高。但是在日常开发中,每一款产品都会有一套自己独特的一套刷新样式。相信有很多小伙伴在个性化定制中都或多或少的遇到过麻烦。今天我就给大家推荐一个在定制方面很出彩的一个刷新框架,该框架自身完成了下拉刷新与上拉加载功能,同时将顶部视图与底部视图的UI定制功能通过接口很方便的提供给使用者自行定义。 相关代码已经上传到上,欢迎star、fork

基本流程

先简单了解一下SwipeToLoadLayout的使用流程,以下拉刷新为例:

  1. 完成Header部分,实现SwipeRefreshTrigger与SwipeRefreshTrigger接口
  2. 完成activity或fragment的布局,在SwipeToLoadLayout节点下配置好Header与下拉目标组件(如RecyclerView等)

这里还是要稍微说一下,因为这个布局过程还是有一定的规则的 首先布局的id是固定的,这个我们在ids.xml中就能看出。框架提供三个View:Header、Target、Footer,分别对应三个位置的View

复制代码

其次onFinishInflate()方法告诉我们,最多只能同时存在这三个View,不能有更多的子View了

@Override    protected void onFinishInflate() {        super.onFinishInflate();        final int childNum = getChildCount();        if (childNum == 0) {            // no child return            return;        } else if (0 < childNum && childNum < 4) {            mHeaderView = findViewById(R.id.swipe_refresh_header);            mTargetView = findViewById(R.id.swipe_target);            mFooterView = findViewById(R.id.swipe_load_more_footer);        } else {            // more than three children: unsupported!            throw new IllegalStateException("Children num must equal or less than 3");        }        if (mTargetView == null) {            return;        }        if (mHeaderView != null && mHeaderView instanceof SwipeTrigger) {            mHeaderView.setVisibility(GONE);        }        if (mFooterView != null && mFooterView instanceof SwipeTrigger) {            mFooterView.setVisibility(GONE);        }    }复制代码

这样你就能得出下一步该怎么来实现了吧?没错肯定是这样的

复制代码

Header的部分尤为重要。我们需在Header上实现SwipeTrigger与SwipeRefreshTrigger接口,接口中的方法分别对应滑动刷新在各个状态下的回调。它们分别为 onPrepare:代表下拉刷新开始的状态 onMove:代表正在滑动过程中的状态 onRelease:代表手指松开后,下拉刷新进入松开刷新的状态 onComplete:代表下拉刷新完成的状态 onReset:代表下拉刷新重置恢复的状态 onRefresh:代表正在刷新中的状态 有了这几个接口,我们就可以完成Header部分的任何动画效果了。当然上拉加载更多的场景,只是把SwipeRefreshTrigger接口换成SwipeLoadMoreTrigger接口而已,其他跟下拉刷新情况完全相同

  1. 在activity或fragment中配置下拉监听事件,并在数据获取完成后主动触发刷新swipeToLoadLayout.setRefreshing(false);完成功能

更深入的部分我们放到源码分析里面再说

看起来好像很简单,那么我们就通过几个小Demo了解一下如何使用吧

仿新浪微博

之所以第一个范例选择新浪微博,是因为它是最传统刷新风格:根据箭头和文字的不同来表明当前不同的状态

如果你在早期研究过PullToRefresh,那么很容易在这个框架基础上实现相应的视图更新功能

先完成头部的定义。WeiboRefreshHeaderView作为头,其实际为一个LinearLayout

class WeiboRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger复制代码

头部布局很简单

复制代码

activity的布局也很简单,把头跟身子一起加在SwipeToLoadLayout里

复制代码

下面就是完成头部动画效果了。新浪微博的这个效果就是视图被下拉到头部高度之后,将箭头位置旋转一下同时更换文字,刷新时展现progressbar即可

class WeiboRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {    var pb_weibo: ProgressBar? = null    var iv_weibo: ImageView? = null    var tv_weibo: TextView? = null    // 是否发生旋转    var rotated = false    private val rotate_up: Animation by lazy {        AnimationUtils.loadAnimation(context, R.anim.rotate_up)    }    private val rotate_down: Animation by lazy {        AnimationUtils.loadAnimation(context, R.anim.rotate_down)    }    constructor(context: Context) : super(context)    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)    override fun onFinishInflate() {        super.onFinishInflate()        pb_weibo = findViewById(R.id.pb_weibo)        iv_weibo = findViewById(R.id.iv_weibo)        tv_weibo = findViewById(R.id.tv_weibo)    }    override fun onReset() {        pb_weibo?.visibility = View.GONE        iv_weibo?.visibility = View.VISIBLE        tv_weibo?.text = "下拉刷新"    }    override fun onComplete() {        tv_weibo?.text = "刷新完成"        pb_weibo?.visibility = View.GONE    }    override fun onRelease() {    }    override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {        if (p0 > SizeUtils.dp2px(60f)) {            if (!rotated) {                rotated = true                tv_weibo?.text = "释放更新"                iv_weibo?.clearAnimation()                iv_weibo?.startAnimation(rotate_up)            }        }        else {            if (rotated) {                rotated = false                tv_weibo?.text = "下拉刷新"                iv_weibo?.clearAnimation()                iv_weibo?.startAnimation(rotate_down)            }        }    }    override fun onPrepare() {    }    override fun onRefresh() {        tv_weibo?.text = "加载中"        iv_weibo?.clearAnimation()        iv_weibo?.visibility = View.GONE        pb_weibo?.visibility = View.VISIBLE    }}复制代码

对照一下上文的刷新周期,应该很好理解

美团外卖

美团外卖是利用ImageView直接播放一段animation直到刷新完成停止。在下拉过程中,该ImageView随着位移的距离变化而发生相应的大小变化

美团外卖动画效果是由一系列的图片组成的,所以与新浪微博效果相比更为简单一些

一样要完成头部视图的定义

class MTRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger复制代码
复制代码

剩下就是完成动画的播放与缩放的处理了

class MTRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {    var iv_mt: ImageView? = null    val animationDrawable: AnimationDrawable by lazy {        iv_mt?.background as AnimationDrawable    }    constructor(context: Context) : super(context)    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)    override fun onFinishInflate() {        super.onFinishInflate()        iv_mt = findViewById(R.id.iv_mt)    }    override fun onReset() {    }    override fun onComplete() {        animationDrawable.stop()    }    override fun onRelease() {    }    override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {        val percent = if (p0 * 1.0f / SizeUtils.dp2px(44f) > 1) 1f else p0 * 1.0f / SizeUtils.dp2px(44f)        iv_mt?.scaleY = (0.3f + 0.7 * percent).toFloat()        iv_mt?.scaleX = (0.3f + 0.7 * percent).toFloat()    }    override fun onPrepare() {        if (!animationDrawable.isRunning) {            animationDrawable.start()        }        iv_mt?.scaleY = 0.3f        iv_mt?.scaleX = 0.3f    }    override fun onRefresh() {        if (!animationDrawable.isRunning) {            animationDrawable.start()        }        iv_mt?.scaleY = 1f        iv_mt?.scaleX = 1f    }}复制代码

代码都很简单,很容易理解

饿了么

饿了么的效果是通过SVG来实现的

饿了么app对资源进行了混淆,所以我拿不到图片,只能随便从其他地方找一个了

一样是Header的编写,这里面有一点不同,我用这个开源框架实现SVG播放进度控制功能

我需要将这个动画效果在下拉刷新的过程中实现

class ElemeRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger复制代码
复制代码

下面就是根据滑动偏移量来处理SVG播放的进度

class ElemeRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {    var pathView_ele: PathView? = null    constructor(context: Context) : super(context)    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)    override fun onFinishInflate() {        super.onFinishInflate()        pathView_ele = findViewById(R.id.pathView_ele)    }    override fun onReset() {    }    override fun onComplete() {        pathView_ele?.setPercentage(1f)    }    override fun onRelease() {    }    override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {        val percent = 1 - (SizeUtils.dp2px(58f) - p0) * 1.0f / SizeUtils.dp2px(58f)        val value = if (percent >= 1) 1f else percent        pathView_ele?.setPercentage(value)    }    override fun onPrepare() {        pathView_ele?.setPercentage(0f)    }    override fun onRefresh() {        pathView_ele?.setPercentage(1f)    }}复制代码

这里你会发出一个疑问,怎么效果与饿了么有的差距?饿了么是滑动到Header完成展开之后就不再继续下滑了,那咱们这个怎么实现呢?那我只能说不好意思,在现有条件下咱们实现不了,只能通过改源码完成

那我们就顺带来阅读源码,看看这个地方怎么改进吧?

源码分析

之前的onFinishInflate咱们就不说了,那个就是告诉我们只能有三个View,分别是Header、Target、Footer

然后是测量阶段,在测量阶段可以得到两个重要的变量mHeaderHeight与mFooterHeight,他们分别代表Header与Footer的高度。同时如果定义的mRefreshTriggerOffset(松开刷新的高度)比Header或Footer的高度小,则修正这个刷新位置

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        // header        if (mHeaderView != null) {            final View headerView = mHeaderView;            measureChildWithMargins(headerView, widthMeasureSpec, 0, heightMeasureSpec, 0);            MarginLayoutParams lp = ((MarginLayoutParams) headerView.getLayoutParams());            mHeaderHeight = headerView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;            if (mRefreshTriggerOffset < mHeaderHeight) {                mRefreshTriggerOffset = mHeaderHeight;            }        }        // target        if (mTargetView != null) {            final View targetView = mTargetView;            measureChildWithMargins(targetView, widthMeasureSpec, 0, heightMeasureSpec, 0);        }        // footer        if (mFooterView != null) {            final View footerView = mFooterView;            measureChildWithMargins(footerView, widthMeasureSpec, 0, heightMeasureSpec, 0);            MarginLayoutParams lp = ((MarginLayoutParams) footerView.getLayoutParams());            mFooterHeight = footerView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;            if (mLoadMoreTriggerOffset < mFooterHeight) {                mLoadMoreTriggerOffset = mFooterHeight;            }        }    }复制代码

在onLayout中对三个视图进行布局

@Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        layoutChildren();        mHasHeaderView = (mHeaderView != null);        mHasFooterView = (mFooterView != null);    }复制代码

这里有一个重要的方法layoutChildren,这个方法就是改变三个视图的位置的。当然这个位置要根据不同的类型来处理,默认情况下我们都是STYLE.CLASSIC类型。

private void layoutChildren() {        final int width = getMeasuredWidth();        final int height = getMeasuredHeight();        final int paddingLeft = getPaddingLeft();        final int paddingTop = getPaddingTop();        final int paddingRight = getPaddingRight();        final int paddingBottom = getPaddingBottom();        if (mTargetView == null) {            return;        }        // layout header        if (mHeaderView != null) {            final View headerView = mHeaderView;            MarginLayoutParams lp = (MarginLayoutParams) headerView.getLayoutParams();            final int headerLeft = paddingLeft + lp.leftMargin;            final int headerTop;            switch (mStyle) {                case STYLE.CLASSIC:                    // classic                    headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;                    break;                case STYLE.ABOVE:                    // classic                    headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;                    break;                case STYLE.BLEW:                    // blew                    headerTop = paddingTop + lp.topMargin;                    break;                case STYLE.SCALE:                    // scale                    headerTop = paddingTop + lp.topMargin - mHeaderHeight / 2 + mHeaderOffset / 2;                    break;                case STYLE.BLEW2CLASSIC:                    // blew2classic                    if (mHeaderOffset > mHeaderHeight) {                        headerTop = paddingTop + lp.topMargin;                    }                    else {                        headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;                    }                    break;                default:                    // classic                    headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;                    break;            }            final int headerRight = headerLeft + headerView.getMeasuredWidth();            final int headerBottom = headerTop + headerView.getMeasuredHeight();            headerView.layout(headerLeft, headerTop, headerRight, headerBottom);        }        // layout target        if (mTargetView != null) {            final View targetView = mTargetView;            MarginLayoutParams lp = (MarginLayoutParams) targetView.getLayoutParams();            final int targetLeft = paddingLeft + lp.leftMargin;            final int targetTop;            switch (mStyle) {                case STYLE.CLASSIC:                    // classic                    targetTop = paddingTop + lp.topMargin + mTargetOffset;                    break;                case STYLE.ABOVE:                    // above                    targetTop = paddingTop + lp.topMargin;                    break;                case STYLE.BLEW:                    // classic                    targetTop = paddingTop + lp.topMargin + mTargetOffset;                    break;                case STYLE.SCALE:                    // classic                    targetTop = paddingTop + lp.topMargin + mTargetOffset;                    break;                case STYLE.BLEW2CLASSIC:                    // classic                    targetTop = paddingTop + lp.topMargin + mTargetOffset;                    break;                default:                    // classic                    targetTop = paddingTop + lp.topMargin + mTargetOffset;                    break;            }            final int targetRight = targetLeft + targetView.getMeasuredWidth();            final int targetBottom = targetTop + targetView.getMeasuredHeight();            targetView.layout(targetLeft, targetTop, targetRight, targetBottom);        }        // layout footer        if (mFooterView != null) {            final View footerView = mFooterView;            MarginLayoutParams lp = (MarginLayoutParams) footerView.getLayoutParams();            final int footerLeft = paddingLeft + lp.leftMargin;            final int footerBottom;            switch (mStyle) {                case STYLE.CLASSIC:                    // classic                    footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;                    break;                case STYLE.ABOVE:                    // classic                    footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;                    break;                case STYLE.BLEW:                    // blew                    footerBottom = height - paddingBottom - lp.bottomMargin;                    break;                case STYLE.SCALE:                    // scale                    footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight / 2 + mFooterOffset / 2;                    break;                case STYLE.BLEW2CLASSIC:                    // blew2classic                    if (mFooterOffset > mFooterHeight) {                        footerBottom = height - paddingBottom - lp.bottomMargin;                    }                    else {                        footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;                    }                    break;                default:                    // classic                    footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;                    break;            }            final int footerTop = footerBottom - footerView.getMeasuredHeight();            final int footerRight = footerLeft + footerView.getMeasuredWidth();            footerView.layout(footerLeft, footerTop, footerRight, footerBottom);        }        if (mStyle == STYLE.CLASSIC                || mStyle == STYLE.ABOVE) {            if (mHeaderView != null) {                mHeaderView.bringToFront();            }            if (mFooterView != null) {                mFooterView.bringToFront();            }        } else if (mStyle == STYLE.BLEW || mStyle == STYLE.SCALE || mStyle == STYLE.BLEW2CLASSIC) {            if (mTargetView != null) {                mTargetView.bringToFront();            }        }    }复制代码

以下拉刷新为例,看这行代码。 paddingTop与lp.topMargin都是0,mHeaderHeight是Header的高度,mHeaderOffset就是手指滑动的距离(这个稍后会有说明)。在下拉过程中,mHeaderOffset的值会越来越大,所以headerTop的值是从-mHeaderHeight开始逐渐增大的,所以headerView会向下逐步移动

headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset复制代码

而Target更为简单,你手指滑动多少它就跟着滑动多少

targetTop = paddingTop + lp.topMargin + mTargetOffset;复制代码

这样能够想象出饿了么滑动到mHeaderHeight高度之后如何处理的吧,请参考我自己定义的style--BLEW2CLASSIC

if (mHeaderOffset > mHeaderHeight) {    headerTop = paddingTop + lp.topMargin;}else {    headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;}复制代码

继续往下来到事件分发部分了

@Override    public boolean dispatchTouchEvent(MotionEvent ev) {        final int action = MotionEventCompat.getActionMasked(ev);        switch (action) {            case MotionEvent.ACTION_CANCEL:            case MotionEvent.ACTION_UP:                // swipeToRefresh -> finger up -> finger down if the status is still swipeToRefresh                // in onInterceptTouchEvent ACTION_DOWN event will stop the scroller                // if the event pass to the child view while ACTION_MOVE(condition is false)                // in onInterceptTouchEvent ACTION_MOVE the ACTION_UP or ACTION_CANCEL will not be                // passed to onInterceptTouchEvent and onTouchEvent. Instead It will be passed to                // child view's onTouchEvent. So we must deal this situation in dispatchTouchEvent                onActivePointerUp();                break;        }        return super.dispatchTouchEvent(ev);    }复制代码

获取事件之后,在手指释放的时候执行onActivePointerUp(),咱们来看看。分别判断了当前是处在下拉以刷新、上拉以加载更多、松开以刷新、松开以加载更多,然后滚动到响应的位置上去。注意在松开状态时,执行了onRelease()回调

private void onActivePointerUp() {        if (STATUS.isSwipingToRefresh(mStatus)) {            // simply return            scrollSwipingToRefreshToDefault();        } else if (STATUS.isSwipingToLoadMore(mStatus)) {            // simply return            scrollSwipingToLoadMoreToDefault();        } else if (STATUS.isReleaseToRefresh(mStatus)) {            // return to header height and perform refresh            mRefreshCallback.onRelease();            scrollReleaseToRefreshToRefreshing();        } else if (STATUS.isReleaseToLoadMore(mStatus)) {            // return to footer height and perform loadMore            mLoadMoreCallback.onRelease();            scrollReleaseToLoadMoreToLoadingMore();        }    }复制代码

随后就是事件拦截的判断。只要你向下滑动时Target确实不能再向下移动了或者向上滑动时Target确实不能再向上移动了,那么SwipeRefreshLayout就把事件拦截,执行onTouchEvent里面的位移操作了

@Override    public boolean onInterceptTouchEvent(MotionEvent event) {        final int action = MotionEventCompat.getActionMasked(event);        switch (action) {            case MotionEvent.ACTION_DOWN:                mActivePointerId = MotionEventCompat.getPointerId(event, 0);                mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);                mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);                // if it isn't an ing status or default status                if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isSwipingToLoadMore(mStatus) ||                        STATUS.isReleaseToRefresh(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {                    // abort autoScrolling, not trigger the method #autoScrollFinished()                    mAutoScroller.abortIfRunning();                    if (mDebug) {                        Log.i(TAG, "Another finger down, abort auto scrolling, let the new finger handle");                    }                }                if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isReleaseToRefresh(mStatus)                        || STATUS.isSwipingToLoadMore(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {                    return true;                }                // let children view handle the ACTION_DOWN;                // 1\. children consumed:                // if at least one of children onTouchEvent() ACTION_DOWN return true.                // ACTION_DOWN event will not return to SwipeToLoadLayout#onTouchEvent().                // but the others action can be handled by SwipeToLoadLayout#onInterceptTouchEvent()                // 2\. children not consumed:                // if children onTouchEvent() ACTION_DOWN return false.                // ACTION_DOWN event will return to SwipeToLoadLayout's onTouchEvent().                // SwipeToLoadLayout#onTouchEvent() ACTION_DOWN return true to consume the ACTION_DOWN event.                // anyway: handle action down in onInterceptTouchEvent() to init is an good option                break;            case MotionEvent.ACTION_MOVE:                if (mActivePointerId == INVALID_POINTER) {                    return false;                }                float y = getMotionEventY(event, mActivePointerId);                float x = getMotionEventX(event, mActivePointerId);                final float yInitDiff = y - mInitDownY;                final float xInitDiff = x - mInitDownX;                mLastY = y;                mLastX = x;                boolean moved = Math.abs(yInitDiff) > Math.abs(xInitDiff)                        && Math.abs(yInitDiff) > mTouchSlop;                boolean triggerCondition =                        // refresh trigger condition                        (yInitDiff > 0 && moved && onCheckCanRefresh()) ||                                //load more trigger condition                                (yInitDiff < 0 && moved && onCheckCanLoadMore());                if (triggerCondition) {                    // if the refresh's or load more's trigger condition  is true,                    // intercept the move action event and pass it to SwipeToLoadLayout#onTouchEvent()                    return true;                }                break;            case MotionEvent.ACTION_POINTER_UP: {                onSecondaryPointerUp(event);                mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);                mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);                break;            }            case MotionEvent.ACTION_UP:            case MotionEvent.ACTION_CANCEL:                mActivePointerId = INVALID_POINTER;                break;        }        return super.onInterceptTouchEvent(event);    }复制代码

下面就是位移过程。 如果当期处于初始STATUS_DEFAULT状态,则进入STATUS_SWIPING_TO_REFRESH,同时回调onPrepare()方法 如果在下拉刷新流程中向上滑动并且滑动偏移量小于0,为了不让Target部分移动到屏幕之外,则将体系流程恢复到初始STATUS_DEFAULT状态,同时使用fixCurrentStatusLayout()方法调整三个View的位置。上拉加载更多流程同理 在正常下拉刷新流程中,如果当期状态是STATUS_SWIPING_TO_REFRESH或者是STATUS_RELEASE_TO_REFRESH,即处于下拉以刷新、松开以刷新状态,如果下拉的距离超过mRefreshTriggerOffset,则进入松开以刷新状态,反之则进入下拉以刷新状态。上拉加载更多流程同理 这时候会触发位移发生fingerScroll()

@Override    public boolean onTouchEvent(MotionEvent event) {        final int action = MotionEventCompat.getActionMasked(event);        switch (action) {            case MotionEvent.ACTION_DOWN:                mActivePointerId = MotionEventCompat.getPointerId(event, 0);                return true;            case MotionEvent.ACTION_MOVE:                // take over the ACTION_MOVE event from SwipeToLoadLayout#onInterceptTouchEvent()                // if condition is true                final float y = getMotionEventY(event, mActivePointerId);                final float x = getMotionEventX(event, mActivePointerId);                final float yDiff = y - mLastY;                final float xDiff = x - mLastX;                mLastY = y;                mLastX = x;                if (Math.abs(xDiff) > Math.abs(yDiff) && Math.abs(xDiff) > mTouchSlop) {                    return true;                }                if (STATUS.isStatusDefault(mStatus)) {                    if (yDiff > 0 && onCheckCanRefresh()) {                        mRefreshCallback.onPrepare();                        setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);                    } else if (yDiff < 0 && onCheckCanLoadMore()) {                        mLoadMoreCallback.onPrepare();                        setStatus(STATUS.STATUS_SWIPING_TO_LOAD_MORE);                    }                } else if (STATUS.isRefreshStatus(mStatus)) {                    if (mTargetOffset <= 0) {                        setStatus(STATUS.STATUS_DEFAULT);                        fixCurrentStatusLayout();                        return true;                    }                } else if (STATUS.isLoadMoreStatus(mStatus)) {                    if (mTargetOffset >= 0) {                        setStatus(STATUS.STATUS_DEFAULT);                        fixCurrentStatusLayout();                        return true;                    }                }                if (STATUS.isRefreshStatus(mStatus)) {                    if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isReleaseToRefresh(mStatus)) {                        if (mTargetOffset >= mRefreshTriggerOffset) {                            setStatus(STATUS.STATUS_RELEASE_TO_REFRESH);                        } else {                            setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);                        }                        fingerScroll(yDiff);                    }                } else if (STATUS.isLoadMoreStatus(mStatus)) {                    if (STATUS.isSwipingToLoadMore(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {                        if (-mTargetOffset >= mLoadMoreTriggerOffset) {                            setStatus(STATUS.STATUS_RELEASE_TO_LOAD_MORE);                        } else {                            setStatus(STATUS.STATUS_SWIPING_TO_LOAD_MORE);                        }                        fingerScroll(yDiff);                    }                }                return true;            case MotionEvent.ACTION_POINTER_DOWN: {                final int pointerIndex = MotionEventCompat.getActionIndex(event);                final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex);                if (pointerId != INVALID_POINTER) {                    mActivePointerId = pointerId;                }                mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);                mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);                break;            }            case MotionEvent.ACTION_POINTER_UP: {                onSecondaryPointerUp(event);                mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);                mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);                break;            }            case MotionEvent.ACTION_UP:            case MotionEvent.ACTION_CANCEL:                if (mActivePointerId == INVALID_POINTER) {                    return false;                }                mActivePointerId = INVALID_POINTER;                break;            default:                break;        }        return super.onTouchEvent(event);    }复制代码

位移无非就是对mTargetOffset进行赋值,同时调整三个View的位置。注意这里调用了onMove()回调

private void fingerScroll(final float yDiff) {        float ratio = mDragRatio;        float yScrolled = yDiff * ratio;        // make sure (targetOffset>0 -> targetOffset=0 -> default status)        // or (targetOffset<0 -> targetOffset=0 -> default status)        // forbidden fling (targetOffset>0 -> targetOffset=0 ->targetOffset<0 -> default status)        // or (targetOffset<0 -> targetOffset=0 ->targetOffset>0 -> default status)        // I am so smart :)        float tmpTargetOffset = yScrolled + mTargetOffset;        if ((tmpTargetOffset > 0 && mTargetOffset < 0)                || (tmpTargetOffset < 0 && mTargetOffset > 0)) {            yScrolled = -mTargetOffset;        }        if (mRefreshFinalDragOffset >= mRefreshTriggerOffset && tmpTargetOffset > mRefreshFinalDragOffset) {            yScrolled = mRefreshFinalDragOffset - mTargetOffset;        } else if (mLoadMoreFinalDragOffset >= mLoadMoreTriggerOffset && -tmpTargetOffset > mLoadMoreFinalDragOffset) {            yScrolled = -mLoadMoreFinalDragOffset - mTargetOffset;        }        if (STATUS.isRefreshStatus(mStatus)) {            mRefreshCallback.onMove(mTargetOffset, false, false);        } else if (STATUS.isLoadMoreStatus(mStatus)) {            mLoadMoreCallback.onMove(mTargetOffset, false, false);        }        updateScroll(yScrolled);    }    private void updateScroll(final float yScrolled) {        if (yScrolled == 0) {            return;        }        mTargetOffset += yScrolled;        if (STATUS.isRefreshStatus(mStatus)) {            mHeaderOffset = mTargetOffset;            mFooterOffset = 0;        } else if (STATUS.isLoadMoreStatus(mStatus)) {            mFooterOffset = mTargetOffset;            mHeaderOffset = 0;        }        if (mDebug) {            Log.i(TAG, "mTargetOffset = " + mTargetOffset);        }        layoutChildren();        invalidate();    }复制代码

最后就是执行结束刷新操作,完成闭环。结束的时候,refreshing值为false,执行onComplete()回调,同时回滚到初始位置

public void setRefreshing(boolean refreshing) {        if (!isRefreshEnabled() || mHeaderView == null) {            return;        }        this.mAutoLoading = refreshing;        if (refreshing) {            if (STATUS.isStatusDefault(mStatus)) {                setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);                scrollDefaultToRefreshing();            }        } else {            if (STATUS.isRefreshing(mStatus)) {                mRefreshCallback.onComplete();                postDelayed(new Runnable() {                    @Override                    public void run() {                        scrollRefreshingToDefault();                    }                }, mRefreshCompleteDelayDuration);            }        }    }复制代码

这里还有一个补充,关于自动滑动方面。自动滚动一般都是通过AutoScroller类,调用其autoScroll()方法来完成,而实际上也是调用Scroller.startScroll()。但是不知道你有没有注意到post(this),它在反复调用这个Runnable的run()来判断滑动是否已经结束。如果没有结束,则通过autoScroll()方法来调用move()回调;如果已经结束,则通过autoScrollFinished()方法来判断下一步应该到达何种状态

private class AutoScroller implements Runnable {        private Scroller mScroller;        private int mmLastY;        private boolean mRunning = false;        private boolean mAbort = false;        public AutoScroller() {            mScroller = new Scroller(getContext());        }        @Override        public void run() {            boolean finish = !mScroller.computeScrollOffset() || mScroller.isFinished();            int currY = mScroller.getCurrY();            int yDiff = currY - mmLastY;            if (finish) {                finish();            } else {                mmLastY = currY;                SwipeToLoadLayout.this.autoScroll(yDiff);                post(this);            }        }        /**         * remove the post callbacks and reset default values         */        private void finish() {            mmLastY = 0;            mRunning = false;            removeCallbacks(this);            // if abort by user, don't call            if (!mAbort) {                autoScrollFinished();            }        }        /**         * abort scroll if it is scrolling         */        public void abortIfRunning() {            if (mRunning) {                if (!mScroller.isFinished()) {                    mAbort = true;                    mScroller.forceFinished(true);                }                finish();                mAbort = false;            }        }        /**         * The param yScrolled here isn't final pos of y.         * It's just like the yScrolled param in the         * {@link #updateScroll(float yScrolled)}         *         * @param yScrolled         * @param duration         */        private void autoScroll(int yScrolled, int duration) {            removeCallbacks(this);            mmLastY = 0;            if (!mScroller.isFinished()) {                mScroller.forceFinished(true);            }            mScroller.startScroll(0, 0, 0, yScrolled, duration);            post(this);            mRunning = true;        }    }复制代码

如果是松开以刷新,则进入刷新状态,同时回调onRefresh()方法 如果是正在刷新状态,则复原,执行onReset()方法 如果是松开以刷新并且通过setRefresh(true)方法进来的,则进入正在刷新状态,执行onRefresh()方法;反之则执行复原操作,执行onReset()方法。 上拉加载更多流程同理

private void autoScrollFinished() {        int mLastStatus = mStatus;        if (STATUS.isReleaseToRefresh(mStatus)) {            setStatus(STATUS.STATUS_REFRESHING);            fixCurrentStatusLayout();            mRefreshCallback.onRefresh();        } else if (STATUS.isRefreshing(mStatus)) {            setStatus(STATUS.STATUS_DEFAULT);            fixCurrentStatusLayout();            mRefreshCallback.onReset();        } else if (STATUS.isSwipingToRefresh(mStatus)) {            if (mAutoLoading) {                mAutoLoading = false;                setStatus(STATUS.STATUS_REFRESHING);                fixCurrentStatusLayout();                mRefreshCallback.onRefresh();            } else {                setStatus(STATUS.STATUS_DEFAULT);                fixCurrentStatusLayout();                mRefreshCallback.onReset();            }        } else if (STATUS.isStatusDefault(mStatus)) {        } else if (STATUS.isSwipingToLoadMore(mStatus)) {            if (mAutoLoading) {                mAutoLoading = false;                setStatus(STATUS.STATUS_LOADING_MORE);                fixCurrentStatusLayout();                mLoadMoreCallback.onLoadMore();            } else {                setStatus(STATUS.STATUS_DEFAULT);                fixCurrentStatusLayout();                mLoadMoreCallback.onReset();            }        } else if (STATUS.isLoadingMore(mStatus)) {            setStatus(STATUS.STATUS_DEFAULT);            fixCurrentStatusLayout();            mLoadMoreCallback.onReset();        } else if (STATUS.isReleaseToLoadMore(mStatus)) {            setStatus(STATUS.STATUS_LOADING_MORE);            fixCurrentStatusLayout();            mLoadMoreCallback.onLoadMore();        } else {            throw new IllegalStateException("illegal state: " + STATUS.getStatus(mStatus));        }        if (mDebug) {            Log.i(TAG, STATUS.getStatus(mLastStatus) + " -> " + STATUS.getStatus(mStatus));        }    }复制代码

源码分析到此结束。怎么样,是不是很简单

参考文章

链接:https://www.jianshu.com/p/fc8c73db72b3

更多文章

相信自己,没有做不到的,只有想不到的

如果你觉得此文对您有所帮助,欢迎入群 QQ交流群 :644196190 微信公众号:终端研发部

转载地址:http://zhqlx.baihongyu.com/

你可能感兴趣的文章
Vue开发与调试工具
查看>>
DES加密解密
查看>>
2017年1月15日,开始认真读书!!!
查看>>
oracle TNS-12549: TNS:operating system resource quota exceeded
查看>>
css背景图片位置:background的position(转)
查看>>
【总结整理】webstorm插件使用
查看>>
微服务(二)hystrix
查看>>
git for windows
查看>>
浅谈oracle中for update 和 for update nowait 和 for update wait x的区别
查看>>
DOS与内存
查看>>
[Micropython]发光二极管制作炫彩跑马灯
查看>>
jQuery获取Select选择的Text和 Value(转)
查看>>
C# System.IO.Path
查看>>
Android中期项目设计
查看>>
CodeForces666E Forensic Examination
查看>>
c#学习内容
查看>>
使用SQL Server Analysis Services数据挖掘的关联规则实现商品推荐功能(一)
查看>>
vue实现首页导航切换不同路由的方式
查看>>
第一周作业
查看>>
wcf实现
查看>>