使用RecyclerView和TabLayout创建动态高度的ViewPager

3

我有一个ViewPager,它有3个页面,每个页面可能包含或不包含RecyclerView。RecyclerView中的项数因用户而异。我按照这个问题制作了一个自定义ViewPager:如何将ViewPager的高度包装到当前片段的高度?

现在我面临的问题有两个:

1. 在切换标签时,每个片段都会调用requestLayout()。因此,当RecyclerView包含大量项时,切换时出现延迟,因为它一遍又一遍地测量选项卡。并且RecyclerView中的项是动态的,意味着它具有滚动行为上的加载更多项。

2. 当RecyclerView没有任何项时,会显示一个TextView来指示缺少项。因为ViewPager包装内容,所以似乎无法在片段中居中TextView。因此,当没有RecyclerView时,我需要ViewPager填充整个高度。

这是我的CustomViewPager代码:


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

这是我的viewpager布局文件:


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

以下是每个片段的布局:
<?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>


如何做到这一点?
无论recyclerview中的项目数量是多少,我该如何快速切换选项卡?如果recyclerview不可用,我又该如何通过填充整个高度居中textview? 希望你能帮忙。谢谢。


你考虑过使用ViewPager2吗?它应该更好地与RecyclerView协作,因为在垂直的RecyclerView中放置一个水平的RecyclerView(ViewPager2基于RecyclerView)是一种常见的模式,并且它被设计来处理这种情况。 - Carson Holzheimer
我的意思是,viewpager的高度是否会根据其子项(动态高度)而改变。假设我有3个选项卡。第一个选项卡可能包含一个recyclerview和1000个项目。第二个选项卡可能包含一个recyclerview和仅3个项目。因此,当我从第一个选项卡切换到第二个选项卡时,第二个选项卡会采用包含1000个项目的第一个选项卡的高度,还是只采用具有3个项目的recycleview的高度?希望你明白。 - WebDiva
视图页面的高度保持不变。我无法想象一个 UI,这不是你想要的。除非你有一个巨大的屏幕,否则视图页面永远不可能高到一次显示1000个项目??? - Carson Holzheimer
据我所知,在Whatsapp上,故事选项卡和联系人选项卡都占用相同的空间(选项卡栏下方的整个屏幕)。RecyclerView也将填满整个屏幕。 - Carson Holzheimer
1
哦,我明白了。为什么你要使用嵌套滚动视图?通常在滚动视图中放置RecyclerView是一个不好的想法,因为它完全禁用了所有回收(必须立即布局列表中的每个项目),所以如果你有超过几个垂直页面的项目,这可能会导致卡顿。在上面的代码中,似乎滚动视图并没有什么用处。 - Carson Holzheimer
显示剩余6条评论
2个回答

4
您的主要问题是在ScrollView中嵌套RecyclerView。这很容易引起性能问题,因为RecyclerView不知道自身的哪一部分当前在屏幕上,所以它会一次性加载所有项目。在onBindViewHolder中添加打印日志以确认此情况。此外,这意味着RecyclerView的高度(因此viewpager的高度)现在是任意的,而不是限制为屏幕大小,这将使得文本无法居中。
要获得所需的布局,我建议使用CoordinatorLayout来替换NestedScrollView。我已经在我的应用程序中成功地做到了这一点,我认为这是完全相同的情况。然后您就不再需要自定义ViewPager了。
以下是使用您的布局的已测试工作示例:
 <?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>

请注意,您必须从RecyclerView中删除此行代码: android:nestedScrollingEnabled="false"

谢谢。我还没有使用过CoordinatorLayouts。我会进行研究并告诉您是否有效。问候。 - WebDiva
抱歉,我无法使其正常工作。我将一个RecyclerView放在ConstraintLayout中,但它在固定位置上滚动。您能否按照以下布局进行设计:CoordinatorLayout -> ConstraintLayout -> RelativeLayout -> TabLayout -> ViewPager. - WebDiva
viewpager 必须在 constraintlayout 内部,位于 AppBarLayout 之下。 - Carson Holzheimer
我发现的最后一个问题是,即使在recyclerview中没有太多的项,页面仍然可以滚动到下面的空白处。viewpager不可滚动,但是直到tablayout的每个视图都允许通过拖动来滚动。这是因为appbarlayout吗? - WebDiva
这是因为AppBarLayout还是ConstraintLayout中的滚动标志吗? - WebDiva
显示剩余8条评论

2

以下是可用的XML代码。您可以尝试使用此代码。添加此属性以使NestedScrollView内容完全占用高度。

android:fillViewport="true"

这是您修改后的主要布局XML代码。
<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>

RecyclerView仍处于固定状态,我需要它可以滚动。 - WebDiva
请确保您的RecyclerView XML代码中包含以下两行:android:layout_height="match_parent" android:nestedScrollingEnabled="false"你也可以参考这个链接:https://dev59.com/Ml0Z5IYBdhLWcg3w3DQW#36977637 - Md. Shofiulla

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接