3

I have a viewpager with 3 pages and each of the pages may or may not contain a recyclerview. The count of items in recyclerview vary from users to users. I have made a custom view pager following this question: How to wrap the height of a ViewPager to the height of its current Fragment?.
The issues that I face now are 2 things:

1. While switching tabs, requestLayout() is called for each of the fragments. So when recyclerview contains a lot of items, there is a lag while switching because It is measuring the tabs again and again. And the items in the recyclerview are dynamic meaning it has a load more item on scroll behavior.

2. When the recyclerview have no items, a textview is shown to indicate the absence of items. Because the viewpager wraps the content, It seems that I cannot center the textview in the fragment. So when there are no recyclerview, I need the viewpager to fill the whole height.

Here is my CustomViewPager code:


public class CustomViewPager extends ViewPager {

    private int currentPagePosition = 0;
    Context context;

    public CustomViewPager(@NonNull Context context) {
        super(context);
        this.context = context;
    }

    public CustomViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
        View child = getChildAt(currentPagePosition);
        if(child != null) {
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            int h = child.getMeasuredHeight();
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY);
        }
        if(heightMeasureSpec != 0) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    public void measureCurrentView(int position) {
        currentPagePosition = position;
        requestLayout();
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return false;
    }
}

Here is my viewpager layout file:


<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <RelativeLayout
            android:layout_width="0dp"
            android:layout_height="250dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            android:background="#F8F8F8"
            android:id="@+id/header"/>

        <com.google.android.material.tabs.TabLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/header"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:id="@+id/tabs">

            <com.google.android.material.tabs.TabItem
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="1ST"
                />
            <com.google.android.material.tabs.TabItem
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="2ND"
                />
            <com.google.android.material.tabs.TabItem
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="3RD"
                />


        </com.google.android.material.tabs.TabLayout>

        <com.example.smilee.adapters.CustomViewPager
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/items"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tabs"

            />
        
    </androidx.constraintlayout.widget.ConstraintLayout>

</androidx.core.widget.NestedScrollView> 

And this is the layout for each of the fragments:

<?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"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@android:color/white">

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/recyclerview"
        android:nestedScrollingEnabled="false"
        android:visibility="gone"/>


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:text="No items to show."
        android:fontFamily="@font/medium"
        android:textColor="#777"
        android:textSize="15dp"
        android:gravity="center"
        android:id="@+id/noItemText"
        android:includeFontPadding="false"
        android:visibility="visible"/>


</androidx.constraintlayout.widget.ConstraintLayout>


How to achieve this?
How can I switch between tabs so fast irrespective of count of items in recyclerview and How can I center the textview by filling the whole height if the recyclerview is not available? Hope you can help. Regards.

WebDiva
  • 133
  • 3
  • 23
  • Have you considered using ViewPager2? It should play much nicer with RecyclerView since a horizontal RecyclerView inside a vertical RecyclerView (ViewPager2 is based on RecyclerView) is a common pattern and it is designed to handle that. – Carson Holzheimer Dec 15 '20 at 04:29
  • But will it change it's height according to the content? – WebDiva Dec 15 '20 at 04:37
  • I'm not sure what you mean. Some diagrams might help – Carson Holzheimer Dec 15 '20 at 04:49
  • What I mean is that will the viewpager's height change according to its children (dynamic height). Suppose I have 3 tabs. The first tab may contain a recyclerview and 1000 items in it. The second may contain a recyclerview and just 3 items. So when I am switching tab from first to second, will the second tab take the height of the first tab which contains 1000 items, or will it just take the height of the recycleview which has 3 items? Hope you understood. – WebDiva Dec 15 '20 at 04:56
  • The height of the viewpager remains the same. I can't imagine a UI where this is not what you want. The viewpager cannot ever be high enough for 1000 items to display on the screen at once, unless you have a gigantic screen??? – Carson Holzheimer Dec 15 '20 at 05:05
  • No. I did not mean that. The recyclerview's height is fixed so that the items are scrollable. The items WONT overflow the recyclerview. What I am currently experiencing is that, in the second tab, there are a lot of spaces to scroll because of the space taken by the first tab. This is a common issue. Take Whatsapp for example. When you switch from contacts tab to stories tab, you can see that the stories tab does not takes as much space as contacts tab (unless you have same number of items in both tabs). This is what I want to acheive. – WebDiva Dec 15 '20 at 05:12
  • I still don't quite understand. Isn't what you want default behaviour? A screenshot would be very helpful. Are you just missing `android:layout_height="match_parent"` on your ViewPager? – Carson Holzheimer Dec 15 '20 at 05:35
  • As far as I can tell, on Whatsapp, both the stories tab and the contacts tab both take the same amount of space (the whole screen below the tab bar). The RecyclerView will also be filling the whole screen. – Carson Holzheimer Dec 15 '20 at 06:48
  • Yes they both take the same amount of space as you said. My layout is in a NestedScrollView. Suppose I have 1000 items in a grid recyclerview. I have scrolled all the way to the bottom. The tablayout is fixed on top and it does not scroll. So when I am at the bottom of the first tab and switch to the second tab, I am at the space place and I have to scroll all the way to top. There are empty space in the second tab as much as the items in the first recyclerview. Which is why there is this custom viewpager class. It requests layout each time I switch tabs and changes the height accordingly. – WebDiva Dec 15 '20 at 10:08
  • 1
    Oh I see. Why do you have a nested scroll view? It's usually a bad idea to have a RecyclerView inside a scrollview as it completely disables all recycling (it has to instantly lay out every item in the list) so that might be causing you lag if you have more than a few vertical pages of items. In the code above it appears like the scrollview isn't very useful – Carson Holzheimer Dec 15 '20 at 11:00
  • This is a profile page (kind of like Instagram & TikTok). I have a relativelayout above the tablayout for displaying profile details and bio. So when the user scroll through the items, the relativelayout is in a fixed state and It does not provide a good ui experience. Hence the use of nestedscrollview. – WebDiva Dec 15 '20 at 14:28

2 Answers2

4

Your main problem is nesting a RecyclerView inside a ScrollView. This can easily cause performance issues since the RecyclerView doesn't know which part of itself is currently on screen, so it loads all items at once. Add a print log inonBindViewHolder to confirm this. Also it means the RecyclerView height (and thus the viewpager height) is now arbitrary and not limited to the size of the screen, which will make it impossible to centre your text.

To achieve the layout you want, I would replace NestedScrollView with a CoordinatorLayout. I've done this successfully in my app for what I think is the exact same situation. Then you would no longer need a custom ViewPager.

CoordinatorLayout with recyclerview below

Here is a tested working example using your layout:

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

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layout_scrollFlags="scroll|enterAlways">

            <RelativeLayout
                android:id="@+id/header"
                android:layout_width="0dp"
                android:layout_height="250dp"
                android:background="#F8F8F8"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

            <com.google.android.material.tabs.TabLayout
                android:id="@+id/tabs"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/header">

            </com.google.android.material.tabs.TabLayout>
        </androidx.constraintlayout.widget.ConstraintLayout>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/items"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_anchor="@id/app_layout"
        app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Note that you must remove this line from RecyclerView: android:nestedScrollingEnabled="false"

Carson Holzheimer
  • 2,890
  • 25
  • 36
  • Thank you. I havent worked with coordinatorlayouts. Will research and let you know if it worked. Regards. – WebDiva Dec 16 '20 at 18:28
  • Can you please give me a simple demo code? I can't make it work. – WebDiva Dec 17 '20 at 16:09
  • Sorry I can't make it work. I put a recyclerview inside the constraintlayout and it scrolls in a fixed position. Can you do the layout like this : CoordinatorLayout -> ConstraintLayout -> RelativeLayout -> TabLayout -> ViewPager – WebDiva Dec 18 '20 at 08:54
  • The viewpager must be inside the constraintlayout, below the AppBarLayout – Carson Holzheimer Dec 18 '20 at 12:20
  • Yes I did it the same. But no help. I didn't use both appbarlayout and toolbar for my layout as it's not necessary. But I tried it, and it didn't work. – WebDiva Dec 18 '20 at 23:18
  • I've updated my answer again. This did take me a few hours to get working :( – Carson Holzheimer Dec 20 '20 at 04:21
  • Thank You!!. It's working perfect. Could you please explain me the use of appbarlayout? Anyway thanks a lot. I have been trying to fix it for the past 2 months. Regards.. – WebDiva Dec 20 '20 at 12:48
  • Fortunately it's all going to get a lot easier with Jetpack Compose :) Appbarlayout works with Coordinatorlayout to give the scroll behaviour. It may be possible to use just the ConstraintLayout and apply a `layout_behavior` to it but I'm not sure – Carson Holzheimer Dec 20 '20 at 13:52
  • When I am centering a linearlayout from a child fragment in the viewpager, it's bottom portion gets below the viewpager. Why is this happening? – WebDiva Dec 20 '20 at 17:06
  • Check this link: https://stackoverflow.com/a/58296374/4672107 – Carson Holzheimer Dec 21 '20 at 00:01
  • One last issue that I found is that even though there aren't much items in the recyclerview, the page is still scrollable to the empty space below. The viewpager is not scrollable but every view till the tablayout allows to scroll, by dragging. Is this because of the appbarlayout? – WebDiva Dec 21 '20 at 10:43
  • Is this because of the appbarlayout, or the scroll flags in constraintlayout? – WebDiva Dec 21 '20 at 10:49
  • I don't know sorry. Maybe try asking another SO question – Carson Holzheimer Dec 22 '20 at 02:50
2

Here is the working XML code. You can try with this code. Add this attribute to make NestedScrollView content full height

android:fillViewport="true"

this is your modified main layout XML code

<androidx.core.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fillViewport="true">

            <androidx.constraintlayout.widget.ConstraintLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent">

                <RelativeLayout
                    android:layout_width="0dp"
                    android:layout_height="250dp"
                    app:layout_constraintTop_toTopOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    android:background="#F8F8F8"
                    android:id="@+id/header"/>

                <com.google.android.material.tabs.TabLayout
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    app:layout_constraintTop_toBottomOf="@id/header"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    android:id="@+id/tabs">

                    <com.google.android.material.tabs.TabItem
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="1ST"
                        />
                    <com.google.android.material.tabs.TabItem
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="2ND"
                        />
                    <com.google.android.material.tabs.TabItem
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="3RD"
                        />


                </com.google.android.material.tabs.TabLayout>

                <androidx.viewpager.widget.ViewPager
                    android:id="@+id/items"
                    android:layout_width="0dp"
                    android:layout_height="0dp"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toBottomOf="@id/tabs"
                    app:layout_constraintBottom_toBottomOf="parent" />

            </androidx.constraintlayout.widget.ConstraintLayout>

        </androidx.core.widget.NestedScrollView>
Md. Shofiulla
  • 2,135
  • 1
  • 13
  • 19
  • The recyclerview is still in a fixed state. I need it to be scrolled. – WebDiva Dec 20 '20 at 12:43
  • Please make sure these two lines in your XML code for your recyclerview android:layout_height="match_parent" android:nestedScrollingEnabled="false" also, you can follow this https://stackoverflow.com/a/36977637/9158688 – Md. Shofiulla Dec 21 '20 at 07:04