3

I made a viewpager2 which has two Fragments, inside each Fragment there is a Recyclerview. The viewpager itself is inside a Nestedscrollview in order to hide the toolbar when scroll up. Here is my code:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>

<com.google.android.material.appbar.AppBarLayout
    android:id="@+id/appbarlayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        app:layout_scrollFlags="scroll|enterAlways">

    </androidx.appcompat.widget.Toolbar>

    <com.google.android.material.tabs.TabLayout
        android:id="@+id/tab_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAlignment="center"
        app:tabGravity="fill"
        app:tabMode="fixed"/>
</com.google.android.material.appbar.AppBarLayout>

<androidx.core.widget.NestedScrollView
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        
        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/pager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"/>

    </LinearLayout>

</androidx.core.widget.NestedScrollView>

<include layout="@layout/material_design_floating_action_menu" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

As I said the viewPager2 have two fragment each of them have a recyclerview. Here problem is, fragment 2 recyclerView take the same height of fragment 1 recyclerView though both recyclerView have different list items and their height should be depends on the list items. I mean, I am expecting these recyclerViews height should act separately based on the list. How can I solve this issue? Please let me know if you need fragment code.

Edit: Activity code which holds the viewPager2

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    initToolbar();

    init();


    viewPager.setAdapter(createCardAdapter());
    new TabLayoutMediator(tabLayout, viewPager,
            new TabLayoutMediator.TabConfigurationStrategy() {
                @Override public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
                    //tab.setText("Tab " + (position + 1));
                    if(position == 0){

                        tab.setText("Home");
                    }else if(position == 1){
                        tab.setText("Events");
                    }
                }
            }).attach();

    RunnTimePermissions.requestForAllRuntimePermissions(this);

    showNotifyDialog();
}

ViewPager Adapter Code:

public class ViewPagerAdapter extends FragmentStateAdapter {
private static final int CARD_ITEM_SIZE = 2;
public ViewPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
    super(fragmentActivity);
}
@NonNull @Override public Fragment createFragment(int position) {

    switch (position){

        case 0:
            return HomeFragment.newInstance("abc","abc");
           // break;
        case 1:
            return EventListFragment.newInstance("abc","abc");
           // break;
    }
    return HomeFragment.newInstance("abc","abc");
}
@Override public int getItemCount() {
    return CARD_ITEM_SIZE;
}
} 

Fragment 1 layout:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.EventListFragment">


<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rv_event_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    android:layout_marginTop="16dp"
    />

 </FrameLayout>

Fragment 2 layout

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.MedicineListFragment">

     
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rv_medicine_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    android:layout_marginTop="16dp"
    />

 </FrameLayout>
Md Jubayer
  • 272
  • 3
  • 13

2 Answers2

1

Alright buddy. I did it! I have the solution to this issue.

First off, here's the official response from Google after I opened an issue. https://issuetracker.google.com/issues/188474850?pli=1

Anyway, on to the fix. Replace the NestedScrollView with this class:

https://gist.github.com/AfzalivE/fdce03eeee8e16203bcc37ba26d7abf3

The idea is to basically create a very light-weight version of NestedScrollView. Instead of messing with child heights, we listen to the children's scroll and either let them scroll, or forward the scrolling to the BottomSheetBehavior. For example when the RecyclerView has been scrolled all the way to the top, then it doesn't consume any of the scrolling, so we can forward that to the bottom sheet so it can scroll. And we only allow the RecyclerView to scroll when the Bottom sheet is expanded.

Also, I must add that this scenario works out of the box with Jetpack Compose + Accompanist-pager so just do that if you really need perfect functionality.

class BottomSheetScrollView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs),
    NestedScrollingParent2 {

    private val TAG = "NestedScroll3"
    private val childHelper = NestedScrollingChildHelper(this).apply {
        isNestedScrollingEnabled = true
    }
    private var behavior: BottomSheetBehavior<*>? = null
    var started = false
    var canScroll = false
    var pendingCanScroll = false
    var dyPreScroll = 0

    init {
        ViewCompat.setNestedScrollingEnabled(this, true)
    }

    private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() {
        override fun onStateChanged(bottomSheet: View, newState: Int) {
            onNextScrollStop(newState == BottomSheetBehavior.STATE_EXPANDED)
            Log.d(
                "BottomSheet",
                "Can scroll CHANGED to: $canScroll, because bottom sheet state is ${
                    getBottomSheetStateString(newState)
                }"
            )
        }

        override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit
    }


    fun onNextScrollStop(canScroll: Boolean) {
        pendingCanScroll = canScroll
    }

    override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
        // ViewPager2's RecyclerView does not participate in this nested scrolling.
        // This allows it to show it's overscroll indicator.
        if (target is RecyclerView) {
            val layoutManager = target.layoutManager as LinearLayoutManager
            if (layoutManager.orientation == LinearLayoutManager.HORIZONTAL) {
                target.isNestedScrollingEnabled = false
            }
        }

        if (!started) {
            Log.d(TAG, "started nested scroll from $target")
            childHelper.startNestedScroll(axes, type)
            started = true
        }

        return true
    }

    override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
        Log.d(TAG, "accepted nested scroll from $target")
    }

    override fun onStopNestedScroll(target: View, type: Int) {
        if (started) {
            childHelper.stopNestedScroll(type)
            started = false
            Log.d(
                TAG,
                "stopped nested scroll from $target, changing canScroll from $canScroll to $pendingCanScroll"
            )
            canScroll = pendingCanScroll
        }
    }

    override fun onNestedScroll(
        target: View,
        dxConsumed: Int,
        dyConsumed: Int,
        dxUnconsumed: Int,
        dyUnconsumed: Int,
        type: Int
    ) {
        Log.d(
            TAG,
            "onNestedScroll: dxC: $dxConsumed, dyC: $dyConsumed, dxU: $dxUnconsumed, dyU: $dyUnconsumed"
        )
        if (dyUnconsumed == dyPreScroll && dyPreScroll < 0) {
            canScroll = false
            Log.d(TAG, "Can scroll CHANGED to: $canScroll, because scrolled to the top of the list")
        }
    }

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
        Log.d(
            TAG,
            "onNestedPreScroll: dx: $dx, dy: $dy, consumed: [ ${consumed.joinToString(", ")} ]"
        )
        if (!canScroll) {
            childHelper.dispatchNestedPreScroll(dx, dy, consumed, null, type)
            // Ensure all dy is consumed to prevent premature scrolling when not allowed.
            consumed[1] = dy
        } else {
            dyPreScroll = dy
        }
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()

        behavior = findBottomSheetBehaviorParent(parent) as BottomSheetBehavior<*>?
        behavior?.addBottomSheetCallback(bottomSheetCallback)
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        behavior?.removeBottomSheetCallback(bottomSheetCallback)
    }

    private fun findBottomSheetBehaviorParent(parent: ViewParent?): CoordinatorLayout.Behavior<*>? {
        if (parent !is View) {
            throw IllegalArgumentException(
                "None of this view's ancestors are associated with BottomSheetBehavior"
            )
        }

        val layoutParams = parent.layoutParams
        return if (layoutParams is CoordinatorLayout.LayoutParams && layoutParams.behavior != null) {
            layoutParams.behavior
        } else {
            findBottomSheetBehaviorParent((parent as View).parent)
        }
    }

    private fun getBottomSheetStateString(state: Int): String {
        return when (state) {
            1 -> "STATE_DRAGGING"
            2 -> "STATE_SETTLING"
            3 -> "STATE_EXPANDED"
            4 -> "STATE_COLLAPSED"
            5 -> "STATE_HIDDEN"
            6 -> "STATE_HALF_EXPANDED"
            else -> "0"
        }
    }
}
Afzal N
  • 2,546
  • 1
  • 26
  • 24
  • I am facing the problem of same height for different fragments created by the viewpager2. In my case, I have many elements (recyclerviews+viewpagers), all wrapped by a NestedScrollView, the last viewpager(attached to a tablayout) has vertical recyclerviews of elements inside of it's pages, the number of elements is different for each page, but the NestedScrollView is measuring the height based on the first scrolled page than applies the same height to other pages. I am not an advanced developer,so I don't fully understand your customized class and I don't know how to apply it to my case – Jamal N Aug 11 '23 at 21:52
-1

I do not understand why you have recyclerview inside FrameLayout .

Here problem is, fragment 2 recyclerView take the same height of fragment 1 recyclerView though both recyclerView have different list items and their height should be depends on the list items

RecyclerView generally always depends on the item_layout until we have not fixed its height n width to match_parent.

In your case you have fixed RecyclerView android:layout_width and android:layout_width to match_parent in both the Fragment .

Try this:

Fragment 1

<FrameLayout 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.EventListFragment">


<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rv_event_list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    />

 </FrameLayout>

Fragment 2

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.MedicineListFragment">

     
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rv_medicine_list"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    />

 </FrameLayout>

P.S: assuming each reacyclerView's item_layout would be having height as wrap_content

chand mohd
  • 2,363
  • 1
  • 14
  • 27