Motion Layout with swipe gesture + SwipeRefreshLayout + RecyclerView 向上滚动的错误行为
Motion Layout with swipe gesture + SwipeRefreshLayout + RecyclerView bug wrong behavior scrolling up
我正在使用 MotionLayout 构建包含 2 个部分的 UI - 顶部带有一些视图,底部带有 SwipeRefresh 和 RecyclerView。此外,我还有一个用于 MotionLayout 的手势 - SwipeRefresh 在向上滑动时向上移动到顶视图上方。问题是当我将 RecyclerView 滚动到底部(顶视图 "collapses")然后到顶部时 - MotionLayout 立即开始反转我的过渡("expand") - 当 RecyclerView 没有完全滚动到顶部时而不是先滚动 RecyclerView。当我的 SwipeRefresh 正在更新或刷新时,它可以正常工作。禁用它会导致刷新布局进度条在没有动画的情况下消失——这不是一个好的解决方案。有什么解决方法吗?
我在浏览 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.
跟踪错误修复
由于缺乏声誉,我无法 post 对@muetzenflo 的回答发表评论,但我挣扎了几个小时试图在我的 MotionLayout 中禁用动画。我将 isInteractionEnabled 设置为 "false",但它不起作用。最后我意识到我使用自定义 MotionLayout 并且可能应该检查它。只有当我添加
if (!isInteractionEnabled) {
return
}
首先检查 onNestedPreScroll() 是否按预期禁用动画工作。
如果 RecyclerView
或 ListView
不是 SwipeRefreshLayout
的直接子代,则会出现此问题。
最简单的解决方案是适当地提供 OnChildScrollUpCallback
实施和 return 结果。在下面的 Kotlin 代码中,refreshLayout
是 SwipeRefreshLayout
,recyclerView
是 RecyclerView
,这在 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 设置为 touchAnchorId 之后,错误消失了
<OnSwipe
motion:dragDirection="dragDown"
motion:touchAnchorId="@+id/swipeContainer"
motion:touchAnchorSide="top" />
androidx.constraintlayout:约束布局:2.0.2
为@muetzenflo 的回答添加更多内容(因为我也没有足够的声誉来发表评论):
target
中的 SwipeRefreshLayout 也将包含一个 CircleImageView
(我假设它是下拉时显示的刷新图标)。 This will sometimes be the first child 布局(如果布局所在的片段已被删除然后稍后添加回来,这似乎会发生),因此 target.getChildAt(0)
将 return 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;
}
我正在使用 MotionLayout 构建包含 2 个部分的 UI - 顶部带有一些视图,底部带有 SwipeRefresh 和 RecyclerView。此外,我还有一个用于 MotionLayout 的手势 - SwipeRefresh 在向上滑动时向上移动到顶视图上方。问题是当我将 RecyclerView 滚动到底部(顶视图 "collapses")然后到顶部时 - MotionLayout 立即开始反转我的过渡("expand") - 当 RecyclerView 没有完全滚动到顶部时而不是先滚动 RecyclerView。当我的 SwipeRefresh 正在更新或刷新时,它可以正常工作。禁用它会导致刷新布局进度条在没有动画的情况下消失——这不是一个好的解决方案。有什么解决方法吗?
我在浏览 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.
跟踪错误修复由于缺乏声誉,我无法 post 对@muetzenflo 的回答发表评论,但我挣扎了几个小时试图在我的 MotionLayout 中禁用动画。我将 isInteractionEnabled 设置为 "false",但它不起作用。最后我意识到我使用自定义 MotionLayout 并且可能应该检查它。只有当我添加
if (!isInteractionEnabled) {
return
}
首先检查 onNestedPreScroll() 是否按预期禁用动画工作。
如果 RecyclerView
或 ListView
不是 SwipeRefreshLayout
的直接子代,则会出现此问题。
最简单的解决方案是适当地提供 OnChildScrollUpCallback
实施和 return 结果。在下面的 Kotlin 代码中,refreshLayout
是 SwipeRefreshLayout
,recyclerView
是 RecyclerView
,这在 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 设置为 touchAnchorId 之后,错误消失了
<OnSwipe
motion:dragDirection="dragDown"
motion:touchAnchorId="@+id/swipeContainer"
motion:touchAnchorSide="top" />
androidx.constraintlayout:约束布局:2.0.2
为@muetzenflo 的回答添加更多内容(因为我也没有足够的声誉来发表评论):
target
中的 SwipeRefreshLayout 也将包含一个 CircleImageView
(我假设它是下拉时显示的刷新图标)。 This will sometimes be the first child 布局(如果布局所在的片段已被删除然后稍后添加回来,这似乎会发生),因此 target.getChildAt(0)
将 return 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;
}