When I try to scroll list, sometimes this works incorrect - BottomSheet intercepts the scroll event and hides.
How to reproduce this:
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.