scwang90 / SmartRefreshLayout

🔥下拉刷新、上拉加载、二级刷新、淘宝二楼、RefreshLayout、OverScroll,Android智能下拉刷新框架,支持越界回弹、越界拖动,具有极强的扩展性,集成了几十种炫酷的Header和 Footer。
https://segmentfault.com/a/1190000010066071
Apache License 2.0
24.79k stars 4.93k forks source link

MotionLayout嵌套SmartRefreshLayout,Footer显示异常 #1521

Open Er3c opened 1 year ago

Er3c commented 1 year ago

使用Motionlayout嵌套SmartRefreshLayout,实现头部收缩效果,布局如下

<androidx.constraintlayout.motion.widget.MotionLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/ctlHeader"
        android:layout_width="0dp"
        android:layout_height="150dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">
    </androidx.constraintlayout.widget.ConstraintLayout>

    <Indicator
        android:id="@+id/indicator"
        android:layout_width="0dp"
        android:layout_height="42dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/ctlHeader" />

    <SmartRefreshLayout
        android:id="@+id/refreshLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/indicator"
        app:srlEnableAutoLoadMore="true"
        app:srlEnableFooterFollowWhenNoMoreData="true"
        app:srlEnableLoadMoreWhenContentNotFull="false">

        <Header />

        <RecyclerView />

        <Footer />
    </com.yuebuy.common.view.YbRefreshLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>

motionScene配置的是向上滑动时收缩header,结果触发loadmore时footer会覆盖卡片,而不是在卡片下方显示。noMoreData状态显示没有问题。 是我使用问题还是bug,期待有人解答

smileToWxm commented 6 months ago

使用Motionlayout嵌套SmartRefreshLayout,实现头部收缩效果,布局如下

<androidx.constraintlayout.motion.widget.MotionLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/ctlHeader"
        android:layout_width="0dp"
        android:layout_height="150dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">
    </androidx.constraintlayout.widget.ConstraintLayout>

    <Indicator
        android:id="@+id/indicator"
        android:layout_width="0dp"
        android:layout_height="42dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/ctlHeader" />

    <SmartRefreshLayout
        android:id="@+id/refreshLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/indicator"
        app:srlEnableAutoLoadMore="true"
        app:srlEnableFooterFollowWhenNoMoreData="true"
        app:srlEnableLoadMoreWhenContentNotFull="false">

        <Header />

        <RecyclerView />

        <Footer />
    </com.yuebuy.common.view.YbRefreshLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>

motionScene配置的是向上滑动时收缩header,结果触发loadmore时footer会覆盖卡片,而不是在卡片下方显示。noMoreData状态显示没有问题。 是我使用问题还是bug,期待有人解答

请问这个问题你解决了吗?我还遇到了另一个问题,就是将嵌套了SmartRefreshLayout的RecyclerView往上滑动时,头部的head就会显示出来,而不是等RecyclerView滑动到了顶部在滑动才显示头部head

mangluan commented 5 months ago

使用Motionlayout嵌套SmartRefreshLayout,实现头部收缩效果,布局如下

<androidx.constraintlayout.motion.widget.MotionLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/ctlHeader"
        android:layout_width="0dp"
        android:layout_height="150dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">
    </androidx.constraintlayout.widget.ConstraintLayout>

    <Indicator
        android:id="@+id/indicator"
        android:layout_width="0dp"
        android:layout_height="42dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/ctlHeader" />

    <SmartRefreshLayout
        android:id="@+id/refreshLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/indicator"
        app:srlEnableAutoLoadMore="true"
        app:srlEnableFooterFollowWhenNoMoreData="true"
        app:srlEnableLoadMoreWhenContentNotFull="false">

        <Header />

        <RecyclerView />

        <Footer />
    </com.yuebuy.common.view.YbRefreshLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>

motionScene配置的是向上滑动时收缩header,结果触发loadmore时footer会覆盖卡片,而不是在卡片下方显示。noMoreData状态显示没有问题。 是我使用问题还是bug,期待有人解答

请问这个问题你解决了吗?我还遇到了另一个问题,就是将嵌套了SmartRefreshLayout的RecyclerView往上滑动时,头部的head就会显示出来,而不是等RecyclerView滑动到了顶部在滑动才显示头部head

这个问题我昨天遇到了,没找到资料,自己看源码解决了,需要代码吗?

在MotionLayout中的onNestedPreScroll方法里,通过target去判断有没有到达顶部,而这里的target是SmartRefreshLayout。 我们可以通过重写SmartRefreshLayout的onNestedPreScroll方法,判断如果父滚动view是MotionLayout的话,把传过去的this改成子view(在这里是RecyclerView)就可以了。 MotionLayout拿到的target是RecyclerView就可以正确判断是否在顶部

smileToWxm commented 5 months ago

使用Motionlayout嵌套SmartRefreshLayout,实现头部收缩效果,布局如下

<androidx.constraintlayout.motion.widget.MotionLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/ctlHeader"
        android:layout_width="0dp"
        android:layout_height="150dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">
    </androidx.constraintlayout.widget.ConstraintLayout>

    <Indicator
        android:id="@+id/indicator"
        android:layout_width="0dp"
        android:layout_height="42dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/ctlHeader" />

    <SmartRefreshLayout
        android:id="@+id/refreshLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/indicator"
        app:srlEnableAutoLoadMore="true"
        app:srlEnableFooterFollowWhenNoMoreData="true"
        app:srlEnableLoadMoreWhenContentNotFull="false">

        <Header />

        <RecyclerView />

        <Footer />
    </com.yuebuy.common.view.YbRefreshLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>

motionScene配置的是向上滑动时收缩header,结果触发loadmore时footer会覆盖卡片,而不是在卡片下方显示。noMoreData状态显示没有问题。 是我使用问题还是bug,期待有人解答

请问这个问题你解决了吗?我还遇到了另一个问题,就是将嵌套了SmartRefreshLayout的RecyclerView往上滑动时,头部的head就会显示出来,而不是等RecyclerView滑动到了顶部在滑动才显示头部head

这个问题我昨天遇到了,没找到资料,自己看源码解决了,需要代码吗?

在MotionLayout中的onNestedPreScroll方法里,通过target去判断有没有到达顶部,而这里的target是SmartRefreshLayout。 我们可以通过重写SmartRefreshLayout的onNestedPreScroll方法,判断如果父滚动view是MotionLayout的话,把传过去的this改成子view(在这里是RecyclerView)就可以了。 MotionLayout拿到的target是RecyclerView就可以正确判断是否在顶部

优秀优秀,我下午按照你的方式试一下,谢谢啦

Er3c commented 5 months ago

使用Motionlayout嵌套SmartRefreshLayout,实现头部收缩效果,布局如下

<androidx.constraintlayout.motion.widget.MotionLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/ctlHeader"
        android:layout_width="0dp"
        android:layout_height="150dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">
    </androidx.constraintlayout.widget.ConstraintLayout>

    <Indicator
        android:id="@+id/indicator"
        android:layout_width="0dp"
        android:layout_height="42dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/ctlHeader" />

    <SmartRefreshLayout
        android:id="@+id/refreshLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/indicator"
        app:srlEnableAutoLoadMore="true"
        app:srlEnableFooterFollowWhenNoMoreData="true"
        app:srlEnableLoadMoreWhenContentNotFull="false">

        <Header />

        <RecyclerView />

        <Footer />
    </com.yuebuy.common.view.YbRefreshLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>

motionScene配置的是向上滑动时收缩header,结果触发loadmore时footer会覆盖卡片,而不是在卡片下方显示。noMoreData状态显示没有问题。 是我使用问题还是bug,期待有人解答

请问这个问题你解决了吗?我还遇到了另一个问题,就是将嵌套了SmartRefreshLayout的RecyclerView往上滑动时,头部的head就会显示出来,而不是等RecyclerView滑动到了顶部在滑动才显示头部head

这个问题我昨天遇到了,没找到资料,自己看源码解决了,需要代码吗?

在MotionLayout中的onNestedPreScroll方法里,通过target去判断有没有到达顶部,而这里的target是SmartRefreshLayout。 我们可以通过重写SmartRefreshLayout的onNestedPreScroll方法,判断如果父滚动view是MotionLayout的话,把传过去的this改成子view(在这里是RecyclerView)就可以了。 MotionLayout拿到的target是RecyclerView就可以正确判断是否在顶部

太帅了,你可以把代码贴出来吗?我刚才看了下你提到的onNestedPreScroll 并不能return view啊

mangluan commented 5 months ago

使用Motionlayout嵌套SmartRefreshLayout,实现头部收缩效果,布局如下

<androidx.constraintlayout.motion.widget.MotionLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/ctlHeader"
        android:layout_width="0dp"
        android:layout_height="150dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent">
    </androidx.constraintlayout.widget.ConstraintLayout>

    <Indicator
        android:id="@+id/indicator"
        android:layout_width="0dp"
        android:layout_height="42dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/ctlHeader" />

    <SmartRefreshLayout
        android:id="@+id/refreshLayout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/indicator"
        app:srlEnableAutoLoadMore="true"
        app:srlEnableFooterFollowWhenNoMoreData="true"
        app:srlEnableLoadMoreWhenContentNotFull="false">

        <Header />

        <RecyclerView />

        <Footer />
    </com.yuebuy.common.view.YbRefreshLayout>
</androidx.constraintlayout.motion.widget.MotionLayout>

motionScene配置的是向上滑动时收缩header,结果触发loadmore时footer会覆盖卡片,而不是在卡片下方显示。noMoreData状态显示没有问题。 是我使用问题还是bug,期待有人解答

请问这个问题你解决了吗?我还遇到了另一个问题,就是将嵌套了SmartRefreshLayout的RecyclerView往上滑动时,头部的head就会显示出来,而不是等RecyclerView滑动到了顶部在滑动才显示头部head

这个问题我昨天遇到了,没找到资料,自己看源码解决了,需要代码吗? 在MotionLayout中的onNestedPreScroll方法里,通过target去判断有没有到达顶部,而这里的target是SmartRefreshLayout。 我们可以通过重写SmartRefreshLayout的onNestedPreScroll方法,判断如果父滚动view是MotionLayout的话,把传过去的this改成子view(在这里是RecyclerView)就可以了。 MotionLayout拿到的target是RecyclerView就可以正确判断是否在顶部

太帅了,你可以把代码贴出来吗?我刚才看了下你提到的onNestedPreScroll 并不能return view啊

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewParent
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.ViewCompat
import androidx.core.view.ViewParentCompat
import com.scwang.smart.refresh.layout.SmartRefreshLayout

class NestedSmartRefreshLayout(context: Context, attrs: AttributeSet?) :
    SmartRefreshLayout(context, attrs) {

    // 本类保存父滚动view
    private var mNestedScrollingParentTouch: ViewParent? = null

//    /**
//     * 获取嵌套滚动父布局方法
//     */
//    private val mNestedChildInGetNestedScrollingParentForTypeMethod by lazy {
//        mNestedChild.javaClass // 获取指定的私有方法对象
//            .getDeclaredMethod("getNestedScrollingParentForType", Int::class.java)
//            .apply {
//                isAccessible = true  // 开启私有调用
//            }
//    }

    /**
     * 调用父类初始化方法,随后获取嵌套滚动父布局
     */
    override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
        // 调用方法,本类保存 mNestedChild 里的滚动父布局
        mNestedScrollingParentTouch =
            getNestedScrollingParent(axes and ViewCompat.SCROLL_AXIS_VERTICAL)
        // 调用父类初始化方法
        super.onNestedScrollAccepted(child, target, axes)

        // 暂时注释掉反射获取,采用循环向上遍历
        // 反射可能会卡,因为在上面一行已经调用父类的开始嵌套滚动方法了,这里才开始初始化

//        // 调用方法,本类保存 mNestedChild 里的滚动父布局
//        val result: Any? = mNestedChildInGetNestedScrollingParentForTypeMethod
//            .invoke(mNestedChild, ViewCompat.TYPE_TOUCH)
//        if (result == null) mNestedScrollingParentTouch = null
//        if (result is ViewParent) mNestedScrollingParentTouch = result
    }

    /**
     * 获取父滚动布局
     */
    private fun getNestedScrollingParent(
        axes: Int, type: Int = ViewCompat.TYPE_TOUCH
    ): ViewParent? {
        if (mNestedChild.isNestedScrollingEnabled) {
            var p: ViewParent? = parent
            var child: View? = this
            while (p != null) {
                if (ViewParentCompat.onStartNestedScroll(p, child!!, this, axes, type)) {
                    return p
                }
                if (p is View) {
                    child = p
                }
                p = p.parent
            }
        }
        return null
    }

    /**
     * 滚动的时候,如果父view是 MotionLayout 则自己处理
     *      把传递给 MotionLayout 的 target 改成真实 target ,用于判断滚动是否到顶部
     */
    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
        if (mNestedScrollingParentTouch !is MotionLayout) {
            super.onNestedPreScroll(target, dx, dy, consumed)
            return
        }
        val parent: MotionLayout = (mNestedScrollingParentTouch ?: return) as MotionLayout

        // If we are in the middle of consuming, a scroll, then we want to move the spinner back up
        // before allowing the list to scroll
        var consumedY = 0

        // dy * mTotalUnconsumed > 0 表示 mSpinner 已经拉出来,现在正要往回推
        // mTotalUnconsumed 将要减去 dy 的距离 再计算新的 mSpinner
        if (dy * mTotalUnconsumed > 0) {
            if (Math.abs(dy) > Math.abs(mTotalUnconsumed)) {
                consumedY = mTotalUnconsumed
                mTotalUnconsumed = 0
            } else {
                consumedY = dy
                mTotalUnconsumed -= dy
            }
            moveSpinnerInfinitely(mTotalUnconsumed.toFloat())
        } else if (dy > 0 && mFooterLocked) {
            consumedY = dy
            mTotalUnconsumed -= dy
            moveSpinnerInfinitely(mTotalUnconsumed.toFloat())
        }

        // Now let our nested parent consume the leftovers
        dispatchNestedPreScroll(parent, target, dx, dy - consumedY, consumed, null)
        consumed[1] += consumedY
    }

    /**
     * 拷贝 NestedScrollingChildHelper 里的 dispatchNestedPreScroll 方法
     *      替换掉 ViewParentCompat.onNestedPreScroll 中的第二个参数 “this” 为 "子target"
     */
    private fun dispatchNestedPreScroll(
        parent: ViewParent, target: View, dx: Int, dy: Int, consumed: IntArray,
        offsetInWindow: IntArray?, @ViewCompat.NestedScrollType type: Int = ViewCompat.TYPE_TOUCH
    ): Boolean {
        if (mNestedChild.isNestedScrollingEnabled) {
            if (dx != 0 || dy != 0) {
                var startX = 0
                var startY = 0
                if (offsetInWindow != null) {
                    this.getLocationInWindow(offsetInWindow)
                    startX = offsetInWindow[0]
                    startY = offsetInWindow[1]
                }
                consumed[0] = 0
                consumed[1] = 0
                ViewParentCompat.onNestedPreScroll(parent, target, dx, dy, consumed, type)
                if (offsetInWindow != null) {
                    this.getLocationInWindow(offsetInWindow)
                    offsetInWindow[0] -= startX
                    offsetInWindow[1] -= startY
                }
                return consumed[0] != 0 || consumed[1] != 0
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0
                offsetInWindow[1] = 0
            }
        }
        return false
    }

}

这个代码只是解决了头部判断是否到顶的问题,footer问题没见过,也没去解决

mangluan commented 5 months ago

你可以用我的代码试试看,如果不能解决问题,可以加我q:508342550,我下午有空