【问题标题】:Android ViewPager with RecyclerView works incorrectly inside BottomSheet带有 RecyclerView 的 Android ViewPager 在 BottomSheet 中无法正常工作
【发布时间】:2016-06-09 02:19:28
【问题描述】:

当我尝试滚动列表时,有时会出错 - 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


    【解决方案1】:

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

    您描述的效果的原因是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 repo 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 时,这也适用)

    【讨论】:

    • 很好的答案。但是,如果将BottomSheetDialogFragment 用作BottomSheet,则不起作用。看看描述这种情况的this thread
    • 这确实是一个有趣的补充!你介意向我的存储库打开一个拉取请求以避免重复公共代码吗? github.com/laenger/ViewPagerBottomSheet
    • 简直太棒了。
    • 我在 26.1 中也遇到了这个问题。方法代码不变。有没有更好的方法来做到这一点?如果我为 viewPager 和 recyclerviews 启用 NestedScrolling,那么对于这两个 recyclerviews,滚动都在底部工作表中起作用。但是,我不确定是否有任何其他副作用。我认为它会影响滑动以消除行为
    • 好吧,以供将来参考 - 你能解释一下你的修复是什么吗?我有自己的自定义行为,不能使用你的,因此我不得不自己分析和复制你的作品。
    【解决方案2】:

    这篇文章救了我的命: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)
            }
        }
    }
    

    【讨论】:

      【解决方案3】:

      还有另一种方法不需要修改BottomSheetBehavior,而是利用BottomSheetBehavior 仅识别它找到的具有NestedScrollingEnabled 的第一个NestedScrollView 的事实。因此,与其在 BottomSheetBehavior 中更改此逻辑,不如启用和禁用适当的滚动视图。我在这里发现了这种方法:https://imnotyourson.com/cannot-scroll-scrollable-content-inside-viewpager-as-bottomsheet-of-coordinatorlayout/

      在我的情况下,我的 BottomSheetBehavior 使用的是 TabLayout 和 FragmentPagerAdapter,所以我的 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();
          }
      

      片段中的任何嵌套滚动视图只需要具有“嵌套”标签即可。

      这是一个示例片段布局文件:

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

      【讨论】:

      • 哇!你为我节省了很多时间!非常感谢您。它解决了我的问题,干杯
      【解决方案4】:

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

      【讨论】:

      • 优雅的解决方案,易于实施。谢谢你:)
      • 你拯救了我的一天。很容易实现
      • 谢谢,我很乐意为您提供帮助。 StackOverflow 给了我很多...
      【解决方案5】:

      我有 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 类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>
      

      【讨论】:

        【解决方案6】:

        看起来只需要适当地更新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 选项卡内布局的复杂性和滚动行为,这比我的其他解决方案效果更好。
        【解决方案7】:

        假设pageNestedScrollView,我可以通过切换其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
            }
        }
        

        【讨论】:

          【解决方案8】:

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

          【讨论】:

            【解决方案9】:

            基于this thread中的Hadi's答案,我们可以这样使用:

            • 我认为这是一个很好的解决方案,我们可以阅读“ (requestDisallowInterceptTouchEvent),当子级不希望此父级及其祖先使用 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)
                  }
              }
              
              

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

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

            【讨论】: