利用NestedScrollingParent2实现RecycleView下拉刷新 上拉加载

/**
 * @author : zyl
 * //感觉这种方式 虽然能实现下啦刷新 上拉加载 但NestedScrollingParent2 这套接口感觉主要是用于嵌套滚动
 */
val BOTTOM_MAX_HEIGHT_DEFAULT = 100.dp
val HEAD_MAX_HEIGHT_DEFAULT = 100.dp

const val SCROLL_DEFAULT_DURATION = 400
const val RESISTANCE_FACTOR_DEFAULT = 0.6

class RefreshLayout(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs),
    NestedScrollingParent2 {

    private val TAG = RefreshLayout::class.java.simpleName

    private var refreshState = RefreshState.NONE
        private set(value) {
            if (field != value) {
                field = value
                refreshStateChangeListener?.refreshStateChange(value)
            }
        }

    private var loadMoreState = LoadState.NONE
        private set(value) {
            if (field != value && canLoadMore.get()) {
                field = value
                refreshStateChangeListener?.loadMoreStateChange(value)
            }
        }

    /**
     * 是否能够触发加载更多的回调
     */
    private val canLoadMore = AtomicBoolean(true)

    private var headHeight = 0

    private var footHeight = 0

    /**
     * 阻力系数
     */
    private var resistance = RESISTANCE_FACTOR_DEFAULT

    private var bottomMaxHeight = BOTTOM_MAX_HEIGHT_DEFAULT

    private var headMaxHeight = HEAD_MAX_HEIGHT_DEFAULT

    private var targetView: View? = null

    private val overScroller = OverScroller(context)

    private val parentHelper = NestedScrollingParentHelper(this).apply {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            isNestedScrollingEnabled = true
        }
    }

    var refreshListener: OnRefreshListener? = null

    /**
     * 刷新状态变化通知 方便外部头部view 做状态变化
     */
    var refreshStateChangeListener: RefreshStateChangeListener? = null

    /**
     * 添加刷新头
     */
    fun addRefreshHeadView(view: View) {
        removeAllViews()
        addView(view)
        addView(targetView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
        requestLayout()
    }

    /**
     * 添加刷新头和尾
     */
    fun addRefreshHeadAndFooter(headView: View, footerView: View) {
        removeAllViews()
        addView(headView)
        addView(targetView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
        addView(footerView)
        requestLayout()
    }

    /**
     * 布局加载完毕了 此时找到加需要刷新头的那个targetView
     */
    override fun onFinishInflate() {
        super.onFinishInflate()
        if (childCount > 1) throw Exception("xml only support one child")
        targetView = getChildAt(0)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var selfWidth = 0
        var selfHeight = 0
        for (index in 0 until childCount) {
            val child = getChildAt(index)
            //忽略margin值的策略
            measureChild(child, widthMeasureSpec, heightMeasureSpec)
            selfWidth = max(child.measuredWidth, selfWidth)
            selfHeight += child.measuredHeight
        }
        setMeasuredDimension(selfWidth, selfHeight)
        //防止没有刷新头的情况
        if (childCount > 1) headHeight = getChildAt(0).measuredHeight
        //得到刷新尾部的高度
        if (childCount > 2) footHeight = getChildAt(2).measuredHeight
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //隐藏头部布局
        var top = -headHeight
        //摆放子控件 竖直排列
        for (index in 0 until childCount) {
            val child = getChildAt(index)
            //居中摆放
            child.layout(
                measuredWidth / 2 - child.measuredWidth / 2,
                top,
                measuredWidth / 2 + child.measuredWidth / 2,
                top + child.measuredHeight
            )
            top += child.measuredHeight
        }
    }

    /**
     * 当是刷新状态时,不允许用户操作
     */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        if (refreshState == RefreshState.REFRESHING
            && ev.actionMasked == MotionEvent.ACTION_DOWN
            && headHeight > 0
        ) return true
        if (loadMoreState == LoadState.LOADING
            && ev.actionMasked == MotionEvent.ACTION_DOWN
            && footHeight > 0
        ) return true
        return super.onInterceptTouchEvent(ev)
    }

    /**
     * 重置列表刷新加载状态
     */
    fun onRefreshDone() {
        if (scrollY > 0) {
            loadMoreState = LoadState.DONE
        } else {
            refreshState = RefreshState.REFRESHING_COMPLETE
        }
        overScroller.startScroll(0, scrollY, 0, -scrollY, SCROLL_DEFAULT_DURATION)
        postInvalidateOnAnimation()
    }


    /**
     * 设置是否触发加载更多 if true 触发
     */
    fun setTriggerLoadMore(trigger: Boolean) {
        canLoadMore.set(trigger)
    }

    override fun computeScroll() {
        if (overScroller.computeScrollOffset()) {
            scrollTo(overScroller.currX, overScroller.currY)
            postInvalidateOnAnimation()
        }
    }


    // NestedScrollingParent2

    override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
        parentHelper.onNestedScrollAccepted(child, target, axes, type)
    }

    override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
        //目前只支持竖直方向
        return nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
    }

    //目前只支持竖直方向
    override fun getNestedScrollAxes(): Int {
        return ViewCompat.SCROLL_AXIS_VERTICAL
    }

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray?, type: Int) {
        //此方法 我的理解 是滑动之前 需要问下父空间是否需要处理 目前没有作为子空间的场景 所以不需要处理
        Log.v(TAG, "onNestedPreScroll target = $target,dy = $dy,scrollY = ${scrollY},type = $type")
        if (scrollY != 0) {
            consumed?.let {
                it[1] = dy
                onNestedScroll(target, dx, 0, 0, dy, TYPE_TOUCH)
            }
        }
    }

    //控制fling
    override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
        return scrollY != 0
    }

    override fun onNestedScroll(
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int
    ) {
        if (dyUnconsumed != 0) {
            //真正处理滑动事件
            var fixDy = fixYOffset(dyUnconsumed)
//            scrollBy(0, fixDy)
            scrollBy(0, fixDy - (fixDy*resistance).toInt())
            if (scrollY < 0) {
                refreshState = if (headHeight > 0 && -scrollY >= headHeight) {
                    RefreshState.RELEASE_TO_REFRESH
                } else {
                    RefreshState.PULL
                }
            } else {
                loadMoreState = if (footHeight in 1..scrollY) {
                    LoadState.RELEASE_TO_LOADING
                } else {
                    LoadState.PULL
                }
            }
        }
        //处理完剩余的事件 看看父亲控件 是否需要 ,此情况暂不考虑
    }

    override fun onStopNestedScroll(target: View, type: Int) {
        Log.v(TAG, "onStopNestedScroll target = $target,scrollY = $scrollY")
        if (refreshState == RefreshState.RELEASE_TO_REFRESH) {
            Log.v(TAG, "onStopNestedScroll refreshing")
            //触发刷新回调
            refreshState = RefreshState.REFRESHING
            //滚动到指定高度
            overScroller.startScroll(0, scrollY, 0, -scrollY - headHeight, SCROLL_DEFAULT_DURATION)
            postInvalidateOnAnimation()
            refreshListener?.reFresh()
        } else if (loadMoreState == LoadState.RELEASE_TO_LOADING) {
            Log.v(TAG, "onStopNestedScroll loadMore")
            loadMoreState = LoadState.LOADING
            //底部滑动到指定位置
            overScroller.startScroll(
                0,
                scrollY,
                0,
                -(scrollY - footHeight),
                SCROLL_DEFAULT_DURATION
            )
            postInvalidateOnAnimation()
            refreshListener?.loadMore()
        } else {
            //不触发刷新 或是加载
            if (scrollY > 0) {
                loadMoreState = LoadState.NONE
            } else {
                refreshState = RefreshState.NONE
            }
            overScroller.startScroll(0, scrollY, 0, -scrollY, SCROLL_DEFAULT_DURATION)
            postInvalidateOnAnimation()
        }
        parentHelper.onStopNestedScroll(target, type)
    }

    /**
     * 修正滑动位置 防止滑动越界
     */
    private fun fixYOffset(dyUnconsumed: Int): Int {
        var fixDy = dyUnconsumed
        if (scrollY < 0) {
            fixDy = if (dyUnconsumed < 0) {
                if (headMaxHeight >= -(dyUnconsumed + scrollY)) {
                    dyUnconsumed
                } else {
                    0
                }
            } else {
                if (dyUnconsumed + scrollY <= 0) {
                    dyUnconsumed
                } else {
                    -scrollY
                }
            }
        } else if (scrollY > 0) {
            fixDy = if (dyUnconsumed > 0) {
                if (bottomMaxHeight >= scrollY + dyUnconsumed) {
                    dyUnconsumed
                } else {
                    0
                }
            } else {
                if (scrollY + dyUnconsumed >= 0) {
                    dyUnconsumed
                } else {
                    -scrollY
                }
            }
        }
        return fixDy
    }

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

推荐阅读更多精彩内容