繁体   English   中英

带有滑动手势的运动布局 + SwipeRefreshLayout + RecyclerView 错误向上滚动的错误行为

[英]Motion Layout with swipe gesture + SwipeRefreshLayout + RecyclerView bug wrong behavior scrolling up

我正在使用 MotionLayout 构建包含 2 个部分的 UI - 顶部带有一些视图,底部带有 SwipeRefresh 和 RecyclerView。 此外,我对 MotionLayout 有一个手势 - SwipeRefresh 在向上滑动时向上移动到顶视图上方。 问题是当我将 RecyclerView 滚动到底部(顶视图“折叠”)然后到顶部时 - MotionLayout 立即开始反转我的转换(“展开”) - 当 RecyclerView 没有完全滚动到顶部而不是滚动 RecyclerView第一的。 当我的 SwipeRefresh 正在更新或刷新时,它可以正常工作。 禁用它会导致刷新布局进度条在没有动画的情况下消失 - 这不是一个好的解决方案。 任何解决方法?

布局 xml 要点

布局场景要点

我在浏览 MotionLayout 的官方错误修复历史时遇到了同样的问题并想出了一个解决方案。 您必须像这样覆盖 MotionLayout 的onNestedPreScroll方法:

/**
 * The current version of motionLayout (2.0.0-beta04) does not honor the position
 * of the RecyclerView, if it is wrapped in a SwipeRefreshLayout.
 * This is the case for the PullRequest screen: When scrolling back to top, the motionLayout transition
 * would be triggered immediately instead of only as soon as the RecyclerView scrolled back to top.
 *
 * This workaround checks if the SwipeRefresh layout can still scroll back up. If so, it does not trigger the motionLayout transition.
 */
class SwipeRefreshMotionLayout : MotionLayout {

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        if (!isInteractionEnabled) {
            return
        }

        if (target !is SwipeRefreshLayout) {
            return super.onNestedPreScroll(target, dx, dy, consumed, type)
        }

        val recyclerView = target.getChildAt(0)
        if (recyclerView !is RecyclerView) {
            return super.onNestedPreScroll(target, dx, dy, consumed, type)
        }

        val canScrollVertically = recyclerView.canScrollVertically(-1)
        if (dy < 0 && canScrollVertically) {
            // don't start motionLayout transition
            return;
        }

        super.onNestedPreScroll(target, dx, dy, consumed, type)
    }
}

将此 MotionLayout 与 SwipeRefreshLayout 结合使用对我来说效果很好。 我也在这里发布了这个,以防你想跟踪谷歌的错误修复。

由于缺乏声誉,我无法对@muetzenflo 答案发表评论,但我挣扎了几个小时试图禁用 MotionLayout 中的动画。 我将 isInteractionEnabled 设置为“false”,但它不起作用。 最后我意识到我使用自定义 MotionLayout 并且可能应该检查它。 只有当我添加

if (!isInteractionEnabled) {
    return
}

作为第一次检查 onNestedPreScroll() 禁用动画按预期工作。

在我将 SwipeRefreshLayout 设置为 touchAnchorId 后,错误消失了

<OnSwipe
motion:dragDirection="dragDown"
motion:touchAnchorId="@+id/swipeContainer"
motion:touchAnchorSide="top" />

androidx.constraintlayout:constraintlayout:2.0.2

在@muetzenflo 的答案中添加更多内容(因为我也没有足够的声誉来发表评论):

target中的 SwipeRefreshLayout 也将包含一个CircleImageView (我假设它是下拉时显示的刷新图标)。 这有时会是布局的第一个子元素(如果布局所在的片段已被删除然后稍后添加回来,这似乎会发生),因此target.getChildAt(0)将返回 CircleImageView 而不是 RecyclerView。 遍历target的所有孩子并检查其中一个是否是 RecyclerView 提供了预期的结果。

代码(Java):

SwipeRefreshLayout swipeLayout = (SwipeRefreshLayout) target;

// Check that the SwipeRefreshLayout has a RecyclerView child
View recyclerView = null;
for (int i = 0; i < swipeLayout.getChildCount(); i++) {
    View child = swipeLayout.getChildAt(i);

    if (child instanceof RecyclerView) {
        recyclerView = child;
        break;
    }
}

if (recyclerView == null) {
    super.onNestedPreScroll(target, dx, dy, consumed, type);
    return;
}

这个解决方案对我有用。 这是基于java中的这两个答案:

https://stackoverflow.com/a/59827320/11962518

https://stackoverflow.com/a/65202877/11962518


binding.motionLayout.setTransitionListener(new MotionLayout.TransitionListener() {
            @Override
            public void onTransitionStarted(MotionLayout motionLayout, int startId, int endId) {

                binding.swipeLayout.setEnabled(false);
            }

            @Override
            public void onTransitionChange(MotionLayout motionLayout, int startId, int endId, float progress) {


                if (progress != 0f) {

                    binding.swipeLayout.setEnabled(false);
                    binding.swipeLayout.setRefreshing(false);
                }
            }

            @Override
            public void onTransitionCompleted(MotionLayout motionLayout, int currentId) {

            }

            @Override
            public void onTransitionTrigger(MotionLayout motionLayout, int triggerId, boolean positive, float progress) {


            }
        });

        binding.recyclerView.setOnTouchListener((view, motionEvent) -> {

            binding.motionLayout.onTouchEvent(motionEvent);
            return false;
        });
           

然后像这样覆盖RecyclerViewonScrolled方法:

        gridLayoutManager = new RtlGridLayoutManager(context, 1);
        adapterRowList = new RecyclerAdapterRowItemCommodity(rowList.getRowItemHome(), context);
        binding.recyclerView.setAdapter(adapterRowList);
        binding.recyclerView.setLayoutManager(gridLayoutManager);
        binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NotNull RecyclerView recyclerView, int dx, int dy) {


                if (!recyclerView.canScrollVertically(-1)) { //Detects start of RecyclerView
                    binding.swipeLayout.setEnabled(true); // now enable swipe refresh layout 
                }

        });

XML:

注意:您应该将您的recyclerView放在另一个layout中,如下所示:


 <androidx.constraintlayout.widget.ConstraintLayout
                android:id="@+id/main_lay"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                app:layout_constraintBottom_toTopOf="@id/view_bottom"
                app:layout_constraintTop_toBottomOf="@id/top_buttons">


                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/recyclerView"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:paddingStart="@dimen/_8cdp"
                    android:paddingEnd="@dimen/_8cdp"
                    />


            </androidx.constraintlayout.widget.ConstraintLayout>

运动场景:

像这样定义Transaction

    <Transition
        motion:constraintSetStart="@id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="1000">

        <OnSwipe motion:touchAnchorId="@id/main_lay"
            motion:touchAnchorSide="top"
            motion:dragDirection="dragUp"/>

    </Transition>

如果RecyclerViewListView不是SwipeRefreshLayout的直接子级,则会出现此问题。

最简单的解决方案是提供OnChildScrollUpCallback实现并适当地返回结果。 在下面的 Kotlin 代码中, refreshLayoutSwipeRefreshLayoutrecyclerViewRecyclerView ,这在xml布局代码中也可以看到。

refreshLayout.setOnChildScrollUpCallback(object : SwipeRefreshLayout.OnChildScrollUpCallback {
  override fun canChildScrollUp(parent: SwipeRefreshLayout, child: View?): Boolean {
    if (recyclerView != null) {
      return recyclerView.canScrollVertically(-1)
    }
    return false
  }
})

虽然xml布局是这样的,

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/swipeRefresh".../>
    ...
    lots of other views i.e TextView, ImageView, MotionLayout
    ...
    ...
    ...
    <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recyclerView".../>
       
    ...
    ...
    ...
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

这也在这里得到解答。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM