• SwipeRefreshLayout,用最少的代码定制最美的上下拉刷新样式


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

    基本流程

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

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

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

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <item name="swipe_target" type="id" />
        <item name="swipe_refresh_header" type="id" />
        <item name="swipe_load_more_footer" type="id" />
    </resources>
    
    

    其次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);
            }
        }
    

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

    <?xml version="1.0" encoding="utf-8"?>
    <com.aspsine.swipetoloadlayout.SwipeToLoadLayout >
        <View
            android:id="@id/swipe_refresh_header" />
        <android.support.v7.widget.RecyclerView
            android:id="@id/swipe_target" />
        <View
            android:id="@id/swipe_load_more_footer" />
    </com.aspsine.swipetoloadlayout.SwipeToLoadLayout>
    
    

    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
    

    头部布局很简单

    <?xml version="1.0" encoding="utf-8"?>
    <com.renyu.swipetoloadlayoutdemo.view.WeiboRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent" android:layout_height="60dip"
        android:gravity="center"
        android:orientation="horizontal">
        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
            <ProgressBar
                android:id="@+id/pb_weibo"
                style="?android:attr/progressBarStyleSmallInverse"
                android:layout_centerInParent="true"
                android:visibility="gone"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
            <ImageView
                android:id="@+id/iv_weibo"
                android:src="@mipmap/tableview_pull_refresh_arrow_down"
                android:layout_centerInParent="true"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
        </RelativeLayout>
        <TextView
            android:id="@+id/tv_weibo"
            android:layout_marginStart="10dip"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="下拉刷新"/>
    </com.renyu.swipetoloadlayoutdemo.view.WeiboRefreshHeaderView>
    

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

    <?xml version="1.0" encoding="utf-8"?>
    <com.aspsine.swipetoloadlayout.SwipeToLoadLayout
        xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/swipe_weibo">
    
        <include
            layout="@layout/header_weibo"
            android:id="@id/swipe_refresh_header" />
        <TextView
            android:id="@id/swipe_target"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="下拉刷新"/>
    </com.aspsine.swipetoloadlayout.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
    
    <?xml version="1.0" encoding="utf-8"?>
    <com.renyu.swipetoloadlayoutdemo.view.MTRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent" android:layout_height="wrap_content"
        android:gravity="center"
        android:padding="10dip">
        <ImageView
            android:id="@+id/iv_mt"
            android:layout_width="112dp"
            android:layout_height="44dp"
            android:background="@drawable/animation_list_refresh_mt"
            android:transformPivotX="56dp"
            android:transformPivotY="22dp"
            android:scaleY="0.3"
            android:scaleX="0.3"/>
    </com.renyu.swipetoloadlayoutdemo.view.MTRefreshHeaderView>
    

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

    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的编写,这里面有一点不同,我用android-pathview这个开源框架实现SVG播放进度控制功能

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

    image
    image
    class ElemeRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
    
    <?xml version="1.0" encoding="utf-8"?>
    <com.renyu.swipetoloadlayoutdemo.view.ElemeRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center">
        <com.eftimoff.androipathview.PathView
            xmlns:app="http://schemas.android.com/apk/res-auto"
            android:id="@+id/pathView_ele"
            android:layout_width="58dp"
            android:layout_height="58dp"
            app:pathColor="@android:color/black"
            app:svg="@raw/issues"
            app:pathWidth="2dp"/>
    </com.renyu.swipetoloadlayoutdemo.view.ElemeRefreshHeaderView>
    

    下面就是根据滑动偏移量来处理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));
            }
        }
    

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

    参考文章
    MNSwipeToLoadDemo

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

    更多文章

    上半年技术文章集合—184篇文章分类汇总

    NDK项目实战—高仿360手机助手之卸载监听

    破解Android版微信跳一跳,一招教你挑战高分

    高级UI特效仿直播点赞效果—一个优美炫酷的点赞动画

    一个实现录音和播放的小案例

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

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

    技术+职场
    技术+职场
  • 相关阅读:
    python for test
    python链接mysql pymysql
    MongoDB数据表添加字段
    NodeVisitor
    无法解决的错误
    一个点绕着另一个点旋转一定角度后的坐标
    2.0版本里程碑,研发日志
    osg Node getParentalNodePaths()报错
    TeslaManage 2.0编译日志
    机械臂模拟2.0
  • 原文地址:https://www.cnblogs.com/gooder2-android/p/9054688.html
Copyright © 2020-2023  润新知