Android ViewPager 与 RecyclerView 在 BottomSheet 中工作不正确

问题描述 投票:0回答:11

当我尝试滚动列表时,有时这工作不正确 - BottomSheet 拦截滚动事件并隐藏。

如何重现这个:

  1. 打开底页
  2. 更改ViewPager的页面
  3. 尝试滚动列表

结果:BottomSheet 将被隐藏。

这是示例代码:

编译'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>

有什么解决方法的想法吗?

android android-viewpager android-recyclerview bottom-sheet
11个回答
34
投票

我遇到了同样的限制,但能够解决它。

您所描述的效果的原因是

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;
}

您可以看到它本质上是使用 DFS 找到第一个滚动子项。

我稍微增强了这个实现,并组装了一个小型以及一个示例应用程序。你可以在这里找到它: 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
作为底部工作表视图:

app:layout_behavior="@string/view_pager_bottom_sheet_behavior"

在底部工作表中设置任何嵌套的 ViewPager:

BottomSheetUtils.setupViewPager(bottomSheetViewPager)

(当 ViewPager 底部工作表视图以及进一步嵌套的 ViewPager 时,这也适用)

sample implementation


7
投票

我最近也遇到过这种情况,我使用了以下自定义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
    }
}


6
投票

这篇文章救了我的命:https://medium.com/@hanru.yeh/funny-solution-that-makes-bottomsheetdialog-support-viewpager-with-nestedscrollingchilds-bfdca72235c3

显示我对底部表格内的 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)
        }
    }
}

6
投票

还有另一种方法不需要修改 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>

2
投票

我有 AndroidX、Kotlin 的解决方案。 已测试并正在处理“com.google.android.material:material:1.1.0-alpha06”。

我还使用了这个:MEDIUM BLOG作为指导。

这是我的 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)
        }
    }
}

最终的解决方案是在类中添加超级构造函数:

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

记住,您必须在下一个路径中添加 ViewPagerBottomSheetBehavior Kotlin ClassPath 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>

2
投票

看起来所需要做的就是适当更新

nestedScrollingChildRef

只需将其设置为

target
中的
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)
    }
}

1
投票

假设

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
    }
}

1
投票

基于Hadi's这个线程中的回答,我们可以这样使用:

  • 我认为这是一个很好的解决方案,我们可以阅读“ (requestDisallowInterceptTouchEvent),当子级不希望父级及其祖先使用 ViewGroup.onInterceptTouchEvent(MotionEvent) 拦截触摸事件时调用。 “来自 android 文档

  • 我测试了几种解决方案,这是一种无错误且无延迟的解决方案,适用于大型列表!

  • 这只是 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)
        }
    }
    
    

然后更改底部工作表容器布局:

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

0
投票

您只需启用滚动到视图寻呼机即可:

ViewCompat.setNestedScrollingEnabled(viewPager2, true);

如果滚动仍然不存在,请将 NestedScrollView 添加到所有 viewpager 子片段中。

存在此问题的原因是,bottomsheet 仅允许滚动到其第一个子视图,并且您必须手动为嵌套的子片段启用滚动。


0
投票

更优雅、非侵入性的解决方案,写为kotlin:

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 (ViewCompat.isNestedScrollingEnabled(view)) {
        return view
    }
    if (view is ViewPager) {
        //通过ViewPagerUtils找到当前在界面上的页面
        val currentViewPagerChild = ViewPagerUtils.getCurrentView(view)
        val scrollingChild = findScrollingChild(currentViewPagerChild)
        return scrollingChild ?: currentViewPagerChild
    } 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


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;
    }
}

使用方法:

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) {

    }
})


-1
投票

我认为将

isNestedScrollingEnabled
中的
ViewPager
属性设置为 true 是解决问题的最简单方法。

© www.soinside.com 2019 - 2024. All rights reserved.