简体   繁体   中英

Android ViewPager with RecyclerView works incorrectly inside BottomSheet

When I try to scroll list, sometimes this works incorrect - BottomSheet intercepts the scroll event and hides.

How to reproduce this:

  1. Open Bottom Sheet
  2. Change a page of ViewPager
  3. Try scroll the list

Result: BottomSheet will be hidden.

Here is sample code:

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

截屏

Any ideas for a workaround?

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:

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.

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

Simply add the maven repo url to your 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:

app:layout_behavior="@string/view_pager_bottom_sheet_behavior"

Setup any nested ViewPager inside the bottom sheet:

BottomSheetUtils.setupViewPager(bottomSheetViewPager)

(This also works when the ViewPager is the bottom sheet view and for further nested ViewPagers)

示例实现

This post saved my life: https://medium.com/@hanru.yeh/funny-solution-that-makes-bottomsheetdialog-support-viewpager-with-nestedscrollingchilds-bfdca72235c3

Show my fix for ViewPager inside bottomsheet.

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/

In my case my BottomSheetBehavior was using a TabLayout with a FragmentPagerAdapter so my FragmentPagerAdapter needed the following code:

@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 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):


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

I have the solution for AndroidX, Kotlin. Tested and working on 'com.google.android.material:material:1.1.0-alpha06'.

I also used this: MEDIUM BLOG as a guide.

Here is My 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)
        }
    }
}

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>

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

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

Simply setting it to the target parameter in onStartNestedScroll is working for me:

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.

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 .

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

     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.

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM