Android 带有 RecyclerView 的 ViewPager 在 BottomSheet 中工作不正确
Android ViewPager with RecyclerView works incorrectly inside BottomSheet
当我尝试滚动列表时,有时这会不正确 - BottomSheet 拦截滚动事件并隐藏。
如何重现:
- 开底Sheet
- 换一个ViewPager页面
- 尝试滚动列表
结果:底部Sheet 将被隐藏。
示例代码如下:
编译'com.android.support:design:23.4.0'
MainActivity.java
package com.nkdroid.bottomsheetsample;
import android.os.Bundle;
import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.TabLayout;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
public
class MainActivity
extends AppCompatActivity
{
private BottomSheetBehavior behavior;
@Override
protected
void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Button btnView = (Button) findViewById(R.id.btnView);
btnView.setOnClickListener(new View.OnClickListener()
{
@Override
public
void onClick(final View v) {
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
final View bottomSheet = findViewById(R.id.bottom_sheet);
behavior = BottomSheetBehavior.from(bottomSheet);
final ViewPager viewPager = (ViewPager) findViewById(R.id.viewPager);
viewPager.setAdapter(new MyPagerAdapter());
final TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(viewPager);
}
private
class MyPagerAdapter
extends PagerAdapter
{
@Override
public
int getCount() {
return 15;
}
@Override
public
Object instantiateItem(final ViewGroup container, final int position) {
final RecyclerView recyclerView = new RecyclerView(MainActivity.this);
recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
recyclerView.setAdapter(new ItemAdapter());
container.addView(recyclerView);
return recyclerView;
}
@Override
public
boolean isViewFromObject(final View view, final Object object) {
return view.equals(object);
}
@Override
public
void destroyItem(final ViewGroup container, final int position, final Object object) {
container.removeView((View) object);
}
@Override
public
CharSequence getPageTitle(final int position) {
return String.valueOf(position);
}
}
public
class ItemAdapter
extends RecyclerView.Adapter<ItemAdapter.ViewHolder>
{
@Override
public
ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new ViewHolder(new TextView(MainActivity.this));
}
@Override
public
void onBindViewHolder(final ViewHolder holder, final int position) {
}
@Override
public
int getItemCount() {
return 100;
}
public
class ViewHolder
extends RecyclerView.ViewHolder
{
public TextView textView;
public
ViewHolder(final View itemView) {
super(itemView);
textView = (TextView) itemView;
}
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout android:id = "@+id/coordinatorLayout"
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"
android:background = "#a3b1ef"
android:fitsSystemWindows = "true"
tools:context = ".ui.MainActivity"
>
<Button
android:id = "@+id/btnView"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
android:text = "Show view"
app:layout_behavior = "@string/appbar_scrolling_view_behavior"
/>
<LinearLayout
android:id = "@+id/bottom_sheet"
android:layout_width = "match_parent"
android:layout_height = "400dp"
android:background = "#fff"
android:gravity = "center"
android:orientation = "vertical"
app:layout_behavior = "@string/bottom_sheet_behavior"
>
<android.support.design.widget.TabLayout
android:id = "@+id/tabs"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
app:tabMode = "scrollable"
/>
<android.support.v4.view.ViewPager
android:id = "@+id/viewPager"
android:layout_width = "match_parent"
android:layout_height = "match_parent"
/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
有什么解决方法吗?
我遇到了同样的限制,但能够解决它。
造成您描述的效果的原因是 BottomSheetBehavior
(从 v24.2.0 开始)仅支持一种滚动 child,它在布局期间按以下方式识别:
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;
}
您可以看到它实际上是使用 DFS 找到第一个滚动 child。
我稍微增强了这个实现并组装了一个小的 库 以及一个示例应用程序。你可以在这里找到它:
https://github.com/laenger/ViewPagerBottomSheet
只需将 Maven 存储库 url 添加到您的 build.gradle:
repositories {
maven { url "https://raw.github.com/laenger/maven-releases/master/releases" }
}
将库添加到依赖项:
dependencies {
compile "biz.laenger.android:vpbs:0.0.2"
}
使用 ViewPagerBottomSheetBehavior
作为底部 sheet 视图:
app:layout_behavior="@string/view_pager_bottom_sheet_behavior"
在底部设置任何嵌套的 ViewPager sheet:
BottomSheetUtils.setupViewPager(bottomSheetViewPager)
(这也适用于 ViewPager 是 底部 sheet 视图和进一步嵌套的 ViewPager)
在 bottomsheet 中显示我对 ViewPager 的修复。
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)
}
}
}
还有另一种方法不需要修改 BottomSheetBehavior,而是利用 BottomSheetBehavior 仅识别它找到的第一个具有 NestedScrollingEnabled 的 NestedScrollView 这一事实。因此,与其在 BottomSheetBehavior 中更改此逻辑,不如启用和禁用适当的滚动视图。我在这里发现了这种方法:https://imnotyourson.com/cannot-scroll-scrollable-content-inside-viewpager-as-bottomsheet-of-coordinatorlayout/
在我的例子中,我的 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();
}
片段中的任何嵌套滚动视图只需要有 "nested" 标签。
这是片段布局文件示例:
<?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>
我有 AndroidX、Kotlin 的解决方案。
已测试并致力于 'com.google.android.material:material:1.1.0-alpha06'。
我也使用了这个:MEDIUM BLOG 作为指南。
这是我的 ViewPagerBottomSheetBehavior Kotlin Class:
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)
}
}
}
最终的解决方案是在 Class 中添加超级构造函数:
constructor() : super()
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
记住,你必须在下一个路径中添加 ViewPagerBottomSheetBehavior Kotlin Class:
Path Class Image reference 因为,你必须覆盖私有方法>
@VisibleForTesting
override fun findScrollingChild(view: View?): View? {
return if (view is ViewPager) {
view.focusedChild?.let { findScrollingChild(it) }
} else {
super.findScrollingChild(view)
}
}
之后可以作为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>
我最近也出现这种情况,我用下面的自定义viewpager class代替了viewpager(on 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
}
}
假设 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
}
}
看来只需要适当地更新 nestedScrollingChildRef
。
只需将其设置为 onStartNestedScroll
中的 target
参数即可为我工作:
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)
}
}
我认为将 ViewPager
中的 isNestedScrollingEnabled
属性 设置为 true 是解决问题的最简单方法。
基于Hadi's answer in 我们可以这样使用:
我认为这是一个很好的解决方案,我们可以阅读“
(requestDisallowInterceptTouchEvent),当 child 不希望此 parent 及其祖先使用 ViewGroup.onInterceptTouchEvent(MotionEvent) 拦截触摸事件时调用。 " 来自 android doc.
我测试了几个解决方案,这是一个无错误且无延迟的解决方案,适用于大型列表!
这只是 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)
}
}
然后更改底部 sheet 容器布局:
<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>
您只需启用滚动到视图寻呼机:
ViewCompat.setNestedScrollingEnabled(viewPager2, true);
如果仍然没有滚动,请将 NestedScrollView 添加到所有 viewpager 子片段中。
存在此问题是因为 bottomsheet 仅允许滚动到其第一个子视图,您必须手动为嵌套的子片段启用滚动。
当我尝试滚动列表时,有时这会不正确 - BottomSheet 拦截滚动事件并隐藏。
如何重现:
- 开底Sheet
- 换一个ViewPager页面
- 尝试滚动列表
结果:底部Sheet 将被隐藏。
示例代码如下:
编译'com.android.support:design:23.4.0'
MainActivity.java
package com.nkdroid.bottomsheetsample;
import android.os.Bundle;
import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.TabLayout;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
public
class MainActivity
extends AppCompatActivity
{
private BottomSheetBehavior behavior;
@Override
protected
void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final Button btnView = (Button) findViewById(R.id.btnView);
btnView.setOnClickListener(new View.OnClickListener()
{
@Override
public
void onClick(final View v) {
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
final View bottomSheet = findViewById(R.id.bottom_sheet);
behavior = BottomSheetBehavior.from(bottomSheet);
final ViewPager viewPager = (ViewPager) findViewById(R.id.viewPager);
viewPager.setAdapter(new MyPagerAdapter());
final TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.setupWithViewPager(viewPager);
}
private
class MyPagerAdapter
extends PagerAdapter
{
@Override
public
int getCount() {
return 15;
}
@Override
public
Object instantiateItem(final ViewGroup container, final int position) {
final RecyclerView recyclerView = new RecyclerView(MainActivity.this);
recyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
recyclerView.setAdapter(new ItemAdapter());
container.addView(recyclerView);
return recyclerView;
}
@Override
public
boolean isViewFromObject(final View view, final Object object) {
return view.equals(object);
}
@Override
public
void destroyItem(final ViewGroup container, final int position, final Object object) {
container.removeView((View) object);
}
@Override
public
CharSequence getPageTitle(final int position) {
return String.valueOf(position);
}
}
public
class ItemAdapter
extends RecyclerView.Adapter<ItemAdapter.ViewHolder>
{
@Override
public
ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new ViewHolder(new TextView(MainActivity.this));
}
@Override
public
void onBindViewHolder(final ViewHolder holder, final int position) {
}
@Override
public
int getItemCount() {
return 100;
}
public
class ViewHolder
extends RecyclerView.ViewHolder
{
public TextView textView;
public
ViewHolder(final View itemView) {
super(itemView);
textView = (TextView) itemView;
}
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout android:id = "@+id/coordinatorLayout"
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"
android:background = "#a3b1ef"
android:fitsSystemWindows = "true"
tools:context = ".ui.MainActivity"
>
<Button
android:id = "@+id/btnView"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
android:text = "Show view"
app:layout_behavior = "@string/appbar_scrolling_view_behavior"
/>
<LinearLayout
android:id = "@+id/bottom_sheet"
android:layout_width = "match_parent"
android:layout_height = "400dp"
android:background = "#fff"
android:gravity = "center"
android:orientation = "vertical"
app:layout_behavior = "@string/bottom_sheet_behavior"
>
<android.support.design.widget.TabLayout
android:id = "@+id/tabs"
android:layout_width = "match_parent"
android:layout_height = "wrap_content"
app:tabMode = "scrollable"
/>
<android.support.v4.view.ViewPager
android:id = "@+id/viewPager"
android:layout_width = "match_parent"
android:layout_height = "match_parent"
/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
有什么解决方法吗?
我遇到了同样的限制,但能够解决它。
造成您描述的效果的原因是 BottomSheetBehavior
(从 v24.2.0 开始)仅支持一种滚动 child,它在布局期间按以下方式识别:
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;
}
您可以看到它实际上是使用 DFS 找到第一个滚动 child。
我稍微增强了这个实现并组装了一个小的 库 以及一个示例应用程序。你可以在这里找到它: https://github.com/laenger/ViewPagerBottomSheet
只需将 Maven 存储库 url 添加到您的 build.gradle:
repositories {
maven { url "https://raw.github.com/laenger/maven-releases/master/releases" }
}
将库添加到依赖项:
dependencies {
compile "biz.laenger.android:vpbs:0.0.2"
}
使用 ViewPagerBottomSheetBehavior
作为底部 sheet 视图:
app:layout_behavior="@string/view_pager_bottom_sheet_behavior"
在底部设置任何嵌套的 ViewPager sheet:
BottomSheetUtils.setupViewPager(bottomSheetViewPager)
(这也适用于 ViewPager 是 底部 sheet 视图和进一步嵌套的 ViewPager)
在 bottomsheet 中显示我对 ViewPager 的修复。
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)
}
}
}
还有另一种方法不需要修改 BottomSheetBehavior,而是利用 BottomSheetBehavior 仅识别它找到的第一个具有 NestedScrollingEnabled 的 NestedScrollView 这一事实。因此,与其在 BottomSheetBehavior 中更改此逻辑,不如启用和禁用适当的滚动视图。我在这里发现了这种方法:https://imnotyourson.com/cannot-scroll-scrollable-content-inside-viewpager-as-bottomsheet-of-coordinatorlayout/
在我的例子中,我的 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();
}
片段中的任何嵌套滚动视图只需要有 "nested" 标签。
这是片段布局文件示例:
<?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>
我有 AndroidX、Kotlin 的解决方案。 已测试并致力于 'com.google.android.material:material:1.1.0-alpha06'。
我也使用了这个:MEDIUM BLOG 作为指南。
这是我的 ViewPagerBottomSheetBehavior Kotlin Class:
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)
}
}
}
最终的解决方案是在 Class 中添加超级构造函数:
constructor() : super()
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
记住,你必须在下一个路径中添加 ViewPagerBottomSheetBehavior Kotlin Class: Path Class Image reference 因为,你必须覆盖私有方法>
@VisibleForTesting
override fun findScrollingChild(view: View?): View? {
return if (view is ViewPager) {
view.focusedChild?.let { findScrollingChild(it) }
} else {
super.findScrollingChild(view)
}
}
之后可以作为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>
我最近也出现这种情况,我用下面的自定义viewpager class代替了viewpager(on 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
}
}
假设 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
}
}
看来只需要适当地更新 nestedScrollingChildRef
。
只需将其设置为 onStartNestedScroll
中的 target
参数即可为我工作:
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)
}
}
我认为将 ViewPager
中的 isNestedScrollingEnabled
属性 设置为 true 是解决问题的最简单方法。
基于Hadi's answer in
我认为这是一个很好的解决方案,我们可以阅读“ (requestDisallowInterceptTouchEvent),当 child 不希望此 parent 及其祖先使用 ViewGroup.onInterceptTouchEvent(MotionEvent) 拦截触摸事件时调用。 " 来自 android doc.
我测试了几个解决方案,这是一个无错误且无延迟的解决方案,适用于大型列表!
这只是 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) } }
然后更改底部 sheet 容器布局:
<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>
您只需启用滚动到视图寻呼机:
ViewCompat.setNestedScrollingEnabled(viewPager2, true);
如果仍然没有滚动,请将 NestedScrollView 添加到所有 viewpager 子片段中。
存在此问题是因为 bottomsheet 仅允许滚动到其第一个子视图,您必须手动为嵌套的子片段启用滚动。