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

Posted

技术标签:

【中文标题】带有滑动手势的运动布局 + SwipeRefreshLayout + RecyclerView 错误错误行为向上滚动【英文标题】:Motion Layout with swipe gesture + SwipeRefreshLayout + RecyclerView bug wrong behavior scrolling up 【发布时间】:2020-07-10 07:54:24 【问题描述】:

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

Layout xml gist

Layout scene gist

【问题讨论】:

我目前在运动布局和 recyclerview+swipe 刷新方面遇到了很多问题。一个问题是 OnSwipe 在干扰 recyclerview 时不起作用,即使我指定了目标区域 ID。另一个更大的问题是,某些子视图有时不会重绘,主要是在打开键盘时。现在就解决所有这些问题。 @frangulyan 也许您当时遇到了 CoordinatorLayout + Appbar 提升问题?我有一个折叠的半透明视图(带有翻译动画,它看起来像 Recycler 在滑动时从底部重叠它),当它折叠时我需要阴影。有默认的 Appbar 高度,但由于我的视图是透明的,我可以从所有 4 个侧面看到阴影,这很烦人。另外我在折叠的工具栏下方有一个视图?像回收站头。尝试在 AppBar 上使用 clipChildren="false" 设置高度,但没有成功 我没有任何这些 - 没有 CoordinatorLayout、没有 AppBars、没有工具栏、没有抽屉,只有普通的全屏视图。虽然我有底部导航,但它位于活动的***别,我看到的问题是导航组件显示的片段。一小时前,我打开了一个问题,描述了我遇到的问题,在这里评论太多了 - ***.com/questions/61014379/… 【参考方案1】:

我在浏览 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 结合使用对我来说效果很好。 我还发布了这个here,以防您想跟踪 Google 的错误修复。

【讨论】:

你有相同的 java 等价物吗? 否,但您也可以在 java 项目中使用此类。 Kotlin 和 Java 文件可以并行使用。【参考方案2】:

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

if (!isInteractionEnabled) 
    return

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

【讨论】:

非常感谢您的意见!我将您的代码添加到了 sn-p。【参考方案3】:

在我将 SwipeRefreshLayout 设置为 touchAnchorId 后,这个 bug 就消失了

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

androidx.constraintlayout:constraintlayout:2.0.2

【讨论】:

在 androidx.constraintlayout:constraintlayout:2.0.4 中对我不起作用【参考方案4】:

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

target 中的 SwipeRefreshLayout 也将包含一个 CircleImageView(我假设它是下拉时显示的刷新图标)。布局的This will sometimes be the first child(如果布局所在的片段已被删除然后稍后添加回来,这似乎会发生),因此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;

【讨论】:

【参考方案5】:

如果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>

这也是here的回答。

【讨论】:

以上是关于带有滑动手势的运动布局 + SwipeRefreshLayout + RecyclerView 错误错误行为向上滚动的主要内容,如果未能解决你的问题,请参考以下文章

使用带有自定义容器视图控制器的滑动手势识别器崩溃

带有自定义后退按钮的滑动手势冻结根视图控制器

Closed:: 创建一个带有滑动和点击手势的 UIButton 水平轮播

Flutter:带有数据的 iOS 向后滑动手势

带有谷歌地图的 Iphone App + UIView 不响应滑动手势

Android_ViewPage_手势滑动