I came across the same limitation but were able to solve it.
我遇到了同样的限制,但我能够解决它。
The reason for the effect you described is that BottomSheetBehavior
(as of v24.2.0) only supports one scrolling child which is identified during layout in the following way:
您所描述的效果的原因是BottomSheetBehavior(从v24.2.0开始)只支持一个滚动子对象,在布局过程中通过以下方式标识:
private View findScrollingChild(View view) {
if (view instanceof NestedScrollingChild) {
return view;
}
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
for (int i = 0, count = group.getChildCount(); i < count; i++) {
View scrollingChild = findScrollingChild(group.getChildAt(i));
if (scrollingChild != null) {
return scrollingChild;
}
}
}
return null;
}
You can see that it essentially finds the first scrolling child using DFS.
您可以看到,它实际上找到了使用DFS的第一个滚动子对象。
I slightly enhanced this implementation and assembled a small library as well as an example app. You can find it here:
https://github.com/laenger/ViewPagerBottomSheet
我稍微增强了这个实现,并组装了一个小型库和一个示例应用程序。你可以在这里找到它:https://github.com/laenger/ViewPagerBottomSheet
Simply add the maven repo url to your build.gradle:
只需将maven repo URL添加到你的build.gradle中:
repositories {
maven { url "https://raw.github.com/laenger/maven-releases/master/releases" }
}
Add the library to the dependencies:
将库添加到依赖项中:
dependencies {
compile "biz.laenger.android:vpbs:0.0.2"
}
Use ViewPagerBottomSheetBehavior
for your bottom sheet view:
将ViewPagerBottomSheetBehavior用于底部工作表视图:
app:layout_behavior="@string/view_pager_bottom_sheet_behavior"
Setup any nested ViewPager inside the bottom sheet:
在底部工作表中设置任何嵌套的ViewPager:
BottomSheetUtils.setupViewPager(bottomSheetViewPager)
(This also works when the ViewPager is the bottom sheet view and for further nested ViewPagers)
(当ViewPager是底部图纸视图和更多嵌套的ViewPages时,这也适用)
I have also been in this situation recently, and I've used the following custom viewpager class instead of the viewpager(on XML), and it worked very well, I think it will help you and others):
我最近也遇到过这种情况,我用下面的自定义viewpager类代替了viewpager(在XML上),它工作得很好,我认为它会对您和其他人有所帮助):
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.viewpager.widget.ViewPager
import java.lang.reflect.Field
class BottomSheetViewPager(context: Context, attrs: AttributeSet?) : ViewPager(context, attrs) {
constructor(context: Context) : this(context, null)
private val positionField: Field =
ViewPager.LayoutParams::class.java.getDeclaredField("position").also {
it.isAccessible = true
}
init {
addOnPageChangeListener(object : SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
requestLayout()
}
})
}
override fun getChildAt(index: Int): View {
val stackTrace = Throwable().stackTrace
val calledFromFindScrollingChild = stackTrace.getOrNull(1)?.let {
it.className == "com.google.android.material.bottomsheet.BottomSheetBehavior" &&
it.methodName == "findScrollingChild"
}
if (calledFromFindScrollingChild != true) {
return super.getChildAt(index)
}
val currentView = getCurrentView() ?: return super.getChildAt(index)
return if (index == 0) {
currentView
} else {
var view = super.getChildAt(index)
if (view == currentView) {
view = super.getChildAt(0)
}
return view
}
}
private fun getCurrentView(): View? {
for (i in 0 until childCount) {
val child = super.getChildAt(i)
val lp = child.layoutParams as? ViewPager.LayoutParams
if (lp != null) {
val position = positionField.getInt(lp)
if (!lp.isDecor && currentItem == position) {
return child
}
}
}
return null
}
}
This post saved my life: https://medium.com/@hanru.yeh/funny-solution-that-makes-bottomsheetdialog-support-viewpager-with-nestedscrollingchilds-bfdca72235c3
这篇帖子救了我的命:https://medium.com/@hanru.yeh/funny-solution-that-makes-bottomsheetdialog-support-viewpager-with-nestedscrollingchilds-bfdca72235c3
Show my fix for ViewPager inside bottomsheet.
显示我对ViewPager Inside Bottom Sheet的修复。
package com.google.android.material.bottomsheet
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.viewpager.widget.ViewPager
import java.lang.ref.WeakReference
class BottomSheetBehaviorFix<V : View> : BottomSheetBehavior<V>(), ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {
val container = viewRef?.get() ?: return
nestedScrollingChildRef = WeakReference(findScrollingChild(container))
}
@VisibleForTesting
override fun findScrollingChild(view: View): View? {
return if (view is ViewPager) {
view.focusedChild?.let { findScrollingChild(it) }
} else {
super.findScrollingChild(view)
}
}
}
There is another approach that does not require modifying BottomSheetBehavior but instead leverages the fact that the BottomSheetBehavior only recognizes the first NestedScrollView with NestedScrollingEnabled it finds. So instead of altering this logic inside BottomSheetBehavior, enable and disable the appropriate scroll views. I discovered this approach here: https://imnotyourson.com/cannot-scroll-scrollable-content-inside-viewpager-as-bottomsheet-of-coordinatorlayout/
还有一种方法不需要修改BottomSheetBehavior,而是利用BottomSheetBehavior只识别它找到的第一个带有NestedScrollingEnabled的NestedScrollView这一事实。因此,与其更改BottomSheetBehavior中的这一逻辑,不如启用和禁用相应的滚动视图。我在这里发现了这种方法:https://imnotyourson.com/cannot-scroll-scrollable-content-inside-viewpager-as-bottomsheet-of-coordinatorlayout/
In my case my BottomSheetBehavior was using a TabLayout with a FragmentPagerAdapter so my FragmentPagerAdapter needed the following code:
在我的例子中,我的BottomSheetBehavior使用带有FragmentPagerAdapter的TabLayout,因此我的FragmentPagerAdapter需要以下代码:
@Override
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
super.setPrimaryItem(container, position, object);
Fragment f = ((Fragment)object);
String activeFragmentTag = f.getTag();
View view = f.getView();
if (view != null) {
View nestedView = view.findViewWithTag("nested");
if ( nestedView != null && nestedView instanceof NestedScrollView) {
((NestedScrollView)nestedView).setNestedScrollingEnabled(true);
}
}
FragmentManager fm = f.getFragmentManager();
for(Fragment frag : fm.getFragments()) {
if (frag.getTag() != activeFragmentTag) {
View v = frag.getView();
if (v!= null) {
View nestedView = v.findViewWithTag("nested");
if (nestedView!= null && nestedView instanceof NestedScrollView) {
((NestedScrollView)nestedView).setNestedScrollingEnabled(false);
}
}
}
}
container.requestLayout();
}
Any nested scroll views in your fragments just need to have the "nested" tag.
片段中的任何嵌套滚动视图只需要有“嵌套”标记。
Here is a sample Fragment layout file:
以下是一个片段布局文件示例:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.MLeftFragment">
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="nested"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- TODO: Update blank fragment layout -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hello_mool_left_fragment" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
I have the solution for AndroidX, Kotlin.
Tested and working on 'com.google.android.material:material:1.1.0-alpha06'.
我有Android X的解决方案,Kotlin。测试和工作'com.google.android.material:material:1.1.0-alpha 06'.
I also used this: MEDIUM BLOG as a guide.
我还使用了这个:Medium博客作为指南。
Here is My ViewPagerBottomSheetBehavior Kotlin Class:
以下是我的ViewPagerBottomSheetBehavior Kotlin类:
package com.google.android.material.bottomsheet
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.viewpager.widget.ViewPager
import java.lang.ref.WeakReference
class ViewPagerBottomSheetBehavior<V : View>
: com.google.android.material.bottomsheet.BottomSheetBehavior<V>,
ViewPager.OnPageChangeListener {
constructor() : super()
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {
val container = viewRef?.get() ?: return
nestedScrollingChildRef = WeakReference(findScrollingChild(container))
}
@VisibleForTesting
override fun findScrollingChild(view: View?): View? {
return if (view is ViewPager) {
view.focusedChild?.let { findScrollingChild(it) }
} else {
super.findScrollingChild(view)
}
}
}
The final solutios was adding the super constructors in the Class:
最终的解决方案是在类中添加超级构造函数:
constructor() : super()
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
Remember, you have to add ViewPagerBottomSheetBehavior Kotlin Class in the next path:
Path Class Image reference because, you must override a private method>
请记住,您必须在下一个路径:Path类图像引用中添加ViewPagerBottomSheetBehavior Kotlin类,因为您必须重写私有方法>
@VisibleForTesting
override fun findScrollingChild(view: View?): View? {
return if (view is ViewPager) {
view.focusedChild?.let { findScrollingChild(it) }
} else {
super.findScrollingChild(view)
}
}
After that, you can use it as a View attribute, like this>
之后,您可以将其用作View属性,如下所示>
<androidx.constraintlayout.widget.ConstraintLayout
app:layout_behavior="com.google.android.material.bottomsheet.ViewPagerBottomSheetBehavior"
android:layout_height="match_parent"
android:layout_width="match_parent">
<include
android:layout_width="match_parent"
android:layout_height="wrap_content"
layout="@layout/you_content_with_a_viewPager_scroll"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Looks like all that's required is updating nestedScrollingChildRef
appropriately.
看起来所需要的只是适当地更新nestedScrollingChildRef。
Simply setting it to the target
parameter in onStartNestedScroll
is working for me:
简单地将其设置为onStartNestedScroll中的目标参数对我来说是有效的:
package com.google.android.material.bottomsheet
class ViewPagerBottomSheetBehavior<V : View>(context: Context, attrs: AttributeSet?) : BottomSheetBehavior<V>(context, attrs) {
override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout, child: V, directTargetChild: View, target: View, axes: Int, type: Int): Boolean {
nestedScrollingChildRef = WeakReference(target)
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type)
}
}
Assuming page
is a NestedScrollView
, I was able to solve the problem by toggling its isNestedScrollingEnabled
property depending on whether or not it's the incoming or outgoing page.
假设Page是一个NestedScrollView,我可以通过切换它的isNestedScrollingEnabled属性来解决这个问题,这取决于它是传入页面还是传出页面。
val viewPager = findViewById<ViewPager>(R.id.viewPager)
viewPager.setPageTransformer(false) { page, position ->
if (position == 0.0f) {
page.isNestedScrollingEnabled = true
} else if (position % 1 == 0.0f) {
page.isNestedScrollingEnabled = false
}
}
Base on Hadi's answer in this thread we can use sth like this:
根据哈迪在这篇帖子中的回答,我们可以使用如下内容:
I think this is a good solution we can read "
(requestDisallowInterceptTouchEvent), Called when a child does not want this parent and its ancestors to intercept touch events with ViewGroup.onInterceptTouchEvent(MotionEvent). " from android doc.
我认为这是一个很好的解决方案,我们可以读到“(QuestDislowInterceptTouchEvent),当一个孩子不想让这个父代及其祖先用ViewGroup.onInterceptTouchEvent(MotionEvent)拦截触摸事件时调用。来自安卓文档。
I tested several solutions and this is a kinda bug free and lag free solution that works on large lists!
我测试了几个解决方案,这是一个在大型列表上工作的无错误和无滞后的解决方案!
this is just the kotlinish way and I changed root view from LinearLayout to ConstraintLayout :
这只是kotlinish的方式,我将根视图从LinearLayout更改为ConstraintLayout:
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.constraintlayout.widget.ConstraintLayout
class DisallowInterceptView : ConstraintLayout {
constructor(context: Context) : super(context) {
requestDisallowInterceptTouchEvent(true)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
requestDisallowInterceptTouchEvent(true)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
requestDisallowInterceptTouchEvent(true)
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
parent.requestDisallowInterceptTouchEvent(true)
return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_MOVE -> requestDisallowInterceptTouchEvent(true)
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> requestDisallowInterceptTouchEvent(false)
}
return super.onTouchEvent(event)
}
}
then change the bottom sheet container layout :
然后更改底部板材容器布局:
<com.vgen.vooop.utils.DisallowInterceptView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
>
.
.
.
</com.vgen.vooop.utils.DisallowInterceptView>
You just have to enable scrolling into the view pager as:
您只需启用滚动到视图分页程序,如下所示:
ViewCompat.setNestedScrollingEnabled(viewPager2, true);
and if scrolling is still absent add NestedScrollView to all your viewpager child fragments.
如果仍然没有滚动,则将NestedScrollView添加到您的所有view pager子片段中。
This problem exists as bottomsheet only enables scrolling to its first child view and you have to manually enable scrolling for your nested child fragments.
这个问题的存在是因为BottomSheet只支持滚动到它的第一个子视图,并且您必须手动启用对嵌套子片段的滚动。
a more elegant and non-invasive solution, wrote as kotlin:
一种更优雅、非侵入性的解决方案,用科特林的话写道:
BottomSheetBehaviorExtension.kt
BottomSheetBehaviorExtension.kt
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.viewpager.widget.ViewPager
import androidx.viewpager.widget.ViewPagerUtils
import com.google.android.material.bottomsheet.BottomSheetBehavior
import java.lang.ref.WeakReference
import java.lang.reflect.Field
private val nestedScrollingChildRef: Field =
BottomSheetBehavior::class.java.getDeclaredField("nestedScrollingChildRef")
fun BottomSheetBehavior<out ViewGroup>.resetScrollingChild(view: View) {
// @Nullable WeakReference<View> nestedScrollingChildRef;
val v = findScrollingChild(view)
nestedScrollingChildRef.isAccessible = true
nestedScrollingChildRef.set(this, WeakReference(v))
}
private fun findScrollingChild(view: View?): View? {
if (view == null || view.visibility != View.VISIBLE) {
return null
}
if (view is ViewPager) {
// 通过 ViewPagerUtils 找到当前在界面上的页面
val currentViewPagerChild = ViewPagerUtils.getCurrentView(view)
val scrollingChild = findScrollingChild(currentViewPagerChild)
return scrollingChild ?: currentViewPagerChild
} else if (ViewCompat.isNestedScrollingEnabled(view)) {
return view
} else if (view is ViewGroup) {
var i = 0
val count = view.childCount
while (i < count) {
val scrollingChild = findScrollingChild(view.getChildAt(i))
if (scrollingChild != null) {
return scrollingChild
}
i++
}
}
return null
}
ViewPagerUtils.java
ViewPagerUtils.java
package androidx.viewpager.widget;
import android.view.View;
public class ViewPagerUtils {
public static View getCurrentView(ViewPager viewPager) {
final int currentItem = viewPager.getCurrentItem();
for (int i = 0; i < viewPager.getChildCount(); i++) {
final View child = viewPager.getChildAt(i);
final ViewPager.LayoutParams layoutParams = (ViewPager.LayoutParams) child.getLayoutParams();
if (!layoutParams.isDecor && currentItem == layoutParams.position) {
return child;
}
}
return null;
}
}
How to use:
如何使用:
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
behavior.resetScrollingChild(viewPager)
}
override fun onPageScrollStateChanged(state: Int) {
}
})
I think setting the isNestedScrollingEnabled
property in ViewPager
to true is the simplest way to solve the issue.
我认为将ViewPager中的isNestedScrollingEnabled属性设置为True是解决该问题的最简单方法。
更多回答
Great answer. However does not work, if BottomSheetDialogFragment
is used as a BottomSheet
. Have a look at this thread where such case is described.
回答得很好。但是,如果将BottomSheetDialogFragment用作BottomSheet,则不起作用。看看这条描述这种情况的帖子。
this is an interesting addition indeed! would you mind opening a pull request to my repository to avoid the duplication of common code? github.com/laenger/ViewPagerBottomSheet
这确实是一个有趣的补充!您介意打开对我的存储库的拉取请求,以避免常见代码的重复吗?Githeb.com/laenger/ViewPagerBottomSheet
I am facing this issue in 26.1 too. The method code is unchanged. Is there any better way to do this? If I enable NestedScrolling for the viewPager and for the recyclerviews, then scrolling works in bottomsheet for both recyclerviews. But, I am not sure about any other side effects. I think it'll affect swipe to dismiss behavior
我在26.1也面临着这个问题。方法代码保持不变。有没有更好的方法来做这件事?如果我为视图寻呼机和回收器视图启用了NestedScrolling,则滚动在底部工作表中对这两个回收器视图都有效。但是,我不确定是否有其他副作用。我觉得刷卡会影响刷卡行为
Well, for the future reference - could you explain a bit on what your fix was? I have my own custom behavior and cannot use yours, thus i am forced to analyze and copy your work myself.
好吧,为了将来的参考-你能解释一下你的解决方案是什么吗?我有自己的习惯行为,不能使用你的,因此我被迫分析和复制自己的工作。
An elegant solution and easy to implement. Thank you :)
优雅的解决方案,易于实施。谢谢:)
You saved my day. So easy to implement
你拯救了我的一天。所以很容易实现
Thanks, I'm happy to help. StackOverflow has given me so much...
谢谢,我很乐意帮忙。StackOverflow给了我太多..。
Hey @Muhammad your solution works fine but until I bundle the app and minify it, it doesn't work anymore on release. I think the issue is due to the class com.google.android.material.bottomsheet.BottomSheetBehavior
is also transformed. so how can I solve it ?
嘿@Muhammad你的解决方案很好用,但在我捆绑应用程序并缩小它之前,它在发布时不再起作用。我觉得这个问题是因为班级com.google.android.material.bottomsheet.BottomSheetBehavior也被改造了。那么我怎么才能解决它呢?
Updates: I just resole the issue by adding this line on proguard-rules.pro file. here: -keep class com.google.android.material.bottomsheet.BottomSheetBehavior
Thanks anyway it now works even on release mode
更新:我只是通过在proGuard-rules.pro文件中添加下面这一行来解决这个问题。这里:-保留类com.google.android.material.bottomsheet.BottomSheetBehavior谢谢无论如何,它现在即使在发布模式下也可以工作
wow man!!! you saved me a lot of time! thank you very much. it solved my problem, cheers
哇,天哪!你为我节省了很多时间!非常感谢。它解决了我的问题,干杯
This worked better than the other solutions in my case due to the complexity and scrolling behavior of the layouts within the tabs of my ViewPager.
在我的情况下,这比其他解决方案效果更好,因为我的ViewPager标签页中布局的复杂性和滚动行为。
我是一名优秀的程序员,十分优秀!