web-dev-qa-db-ja.com

Android RecyclerViewを備えたViewPagerがBottomSheet内で正しく機能しない

リストをスクロールしようとすると、時々これが正しく機能しません-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>

Screenshot

回避策のアイデアはありますか?

16
Vitaly

私は同じ制限に出くわしましたが、それを解決することができました。

あなたが説明した効果の理由は、BottomSheetBehavior(v24.2.0現在)は、レイアウト中に次の方法で識別される1つのスクロール子のみをサポートするためです。

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がボトムシートビューである場合、およびさらにネストされたViewPagersでも機能します)

sample implementation

25
laenger

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

フラグメント内のネストされたスクロールビューには、「ネストされた」タグが必要です。

以下は、サンプルのFragmentレイアウトファイルです。

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

私も最近この状況にあり、viewpager(on XML)の代わりに次のカスタムviewpagerクラスを使用しましたが、それは非常にうまく機能し、あなたや他の人を助けると思います):


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

この投稿は私の人生を救いました: https://medium.com/@hanru.yeh/funny-solution-that-makes-bottomsheetdialog-support-viewpager-with-nestedscrollingchilds-bfdca72235c

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)
        }
    }
}
2
lymoge

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

その後、次のようにビュー属性として使用できます>

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