ViewPager2 中项目的阴影被剪裁

Item's shadow inside a ViewPager2 is clipped

我有一个 activity 有一个 NavHostFragment。此 NavHostFragment 将承载三个片段,其中两个是 FragmentAFragmentB。在 FragmentB 中,我有一个 ViewPager2,它有两个页面:PageAPageB,它们实际上都是从一个片段 FragmentC 构建的。在每个 PageAPageB 中,我有一个 RecyclerView.

这是 FragmentB 的布局 XML。

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/keyline_4"
        android:clipChildren="false"
        android:clipToPadding="false"
        tools:context=".ui.NavigationActivity">

        ...

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/frag_course_view_pager"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:layout_marginTop="@dimen/keyline_4"
            android:clipChildren="false"
            ... />

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/frag_course_tablayout"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/keyline_4"
            ... />


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

如您所见,我已将片段的根布局 clipChildren 设置为 false。我还将 ViewPager2clipChildren 设置为 false。这是 PageAPageB 的布局 XML(即 FragmentC)。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="viewmodel"
            type="com.mobile.tugasakhir.viewmodels.course.CourseTabViewModel" />
    </data>

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/frag_course_tab_rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingBottom="@dimen/offset_bottom_nav_bar_padding"
        android:clipChildren="false"
        android:clipToPadding="false"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

</layout>

如您所见,它只有 RecyclerView,我已将 clipChildren 设置为 false。 RV 的 ViewHolder 的膨胀 XML 布局如下。

<?xml version="1.0" encoding="utf-8"?>
<layout
    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">

    <data>
        ...
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/component_course_container"
        android:layout_width="match_parent"
        android:layout_height="@dimen/course_card_height"
        android:background="@drawable/drawable_rounded_rect"
        android:backgroundTint="?attr/colorSurface"
        android:elevation="@dimen/elevation_0">
        ...

        ...

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

裁剪部分是上述 ViewHolder 的 XML 布局的 elevation 的阴影。因为我已经将所有 parents 的所有 clipChildren 属性设置为 false,阴影不应该被剪裁,但它仍然是。为什么会这样?如何在不更改 padding/margin 的情况下防止它被剪裁 ?

注:我在FragmentA里面也有一个RecyclerView,不同的是FragmentA里面的RecyclerView未嵌套在 ViewPager2 中。按照 FragmentA 上的方法(将所有 parents' clipChildren 设置为 false)允许 RecyclerView 的项目显示它们的阴影。

这是问题的图片。


更新

使用 Layout Inspector,似乎在 ViewPager2 里面,有更多的 ViewGroups(用红色矩形标记)。我的 RecyclerView 的项目被剪掉了,用绿色矩形标记。这是布局检查器显示的内容。

可以看出,在ViewPager2里面,有一个ViewPager2$RecyclerViewImpl,里面有一个FrameLayout(我没有创建这些ViewGroups)。结果这两个 clipChildren 设置为 true 即使 ViewPager2clipChildren 设置为 false。我可以像这样在我的 FragmentB 中定位 ViewPager2$RecyclerViewImpl

(viewPager.getChildAt(0) as ViewGroup).clipChildren = false

然后我尝试使用类似的方法瞄准 FrameLayout

((viewPager.getChildAt(0) as ViewGroup).getChildAt(0) as ViewGroup).clipChildren = false

但是,我得到一个错误,说第二个 getChildAt(0) returns null。在我的布局检查器中,它清楚地表明在我的 RecyclerView 之前有一个 FrameLayout。此 FrameLayoutclipChildren 设置为 true。我很确定我必须将 FrameLayoutclipChildren 设置为 false 以使阴影不被剪裁,但我可能错了。

以下是我成功将 RecyclerViewImplclipChildren 设置为 false 而未能将 FrameLayoutclipChildren 设置为 false分别.

或者有没有更好的方法来取消阴影?

我出于私人原因遮挡了布局预览;这会导致布局预览为白框。


更新 2

对于那些想直接查看问题的人,只需 运行 我在此 Github link 中提供的应用程序即可。使用布局检查器查看我所看到的内容。

clipToPadding="false" 添加到为 ViewHolder 扩充的 xml 文件中的根目录 ConstraintLayout

在更详细地查看您的问题后,我认为问题是 ViewHolder XML 使用 Drawable 作为背景试图模拟投影。

原来 Android 上的阴影不是 "real shadows" 而是框架伪造的绘制元素。这些阴影在某些情况下使用框架添加的额外填充;再次。

CardViews,有一个内部机制来处理这个问题(特别是在没有海拔概念的平台上)。

因此,我建议您将 ViewHOlder 更改为包含 CardView。在这种情况下,它看起来像:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/component_course_container"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:backgroundTint="#efefef">

    <androidx.cardview.widget.CardView
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:cardCornerRadius="8dp"
        app:cardUseCompatPadding="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

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

            <TextView
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:text="Your Content Goes Here"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.cardview.widget.CardView>

</androidx.constraintlayout.widget.ConstraintLayout>

然后... 删除 all 和 clipChildren/CliptoPadding 等。这些会让你比解决方案更头疼,并且会降低性能(框架现在需要绘制额外的东西,否则不会被渲染,因为它被覆盖了)。

希望对您有所帮助。

关于 FrameLayout,这是一个副总裁 "thing",我希望 Google 在最新的 AppCompat 实现中用新的 "FragmentContainer" 替换它。

祝你好运。

这是现在的样子:

这个答案直接解释了我们如何将 auto-generated 有问题的 FrameLayoutclipChildren 定位为 false;从而取消项目的阴影。如果您不介意修改当前的 XML 布局,请查看 (使用 CardView 绘制未剪裁的阴影)也可以解决此问题。


正如所怀疑的那样,提到的 FrameLayout 是导致 children 被剪裁的原因。 FrameLayout 由扩展 class RecyclerView.Adapter<FragmentViewHolder> 的 class FragmentStateAdapter 创建。根据我收集到的信息,这个 FragmentViewHolder 基本上与普通 RecyclerView 的视图持有者类似。 FragmentViewHolder,目前,总是 return 是 FrameLayout(即 ViewGroup)。 view holder 将始终 return a ViewGroup 这一事实似乎即使在未来也不太可能改变。


依赖关系androidx.viewpager2:viewpager2:1.0.0

如果您使用上面的依赖项,您可以看到 FrameLayoutFragmentStateAdapter class 的 onCreateViewHolder 函数中创建(第 160 行)。

@NonNull
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return FragmentViewHolder.create(parent);
}

FragmentViewHolder.create(parent) 方法将始终创建一个 FrameLayout 视图持有者。这将在 FragmentStateAdapteronBindViewHolder 中作为参数 (holder) 传递。 FragmentViewHolder.create的方法声明如下。

@NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
    FrameLayout container = new FrameLayout(parent.getContext());
    container.setLayoutParams(
            new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT));
    container.setId(ViewCompat.generateViewId());
    container.setSaveEnabled(false);
    return new FragmentViewHolder(container);
}

clipChildren 属性设置为 false

我们现在知道 holder 将作为参数传递到 FragmentStateAdapteronBindViewHolder 方法中,我们可以覆盖 onBindViewHolder(在您自己的适配器中class 扩展了 FragmentStateAdapter),将 holder.itemViewclipChildren 属性设置为 false 瞧,RecyclerView 内的项目将不再被剪裁。

我的适配器 class 的最终代码如下。

class PagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
    override fun getItemCount(): Int = 1

    override fun createFragment(position: Int): Fragment {
        return PageFragment().apply {
            arguments = Bundle()
        }
    }

    // Setting its clipChildren to false
    override fun onBindViewHolder(
        holder: FragmentViewHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {
        (holder.itemView as ViewGroup).clipChildren = false
        super.onBindViewHolder(holder, position, payloads)
    }
}

层次结构上方的其他 parent 布局具有小于阴影所需区域的边界框,也需要将其 clipChildren 设置为 false(即 RecyclerViewImpl parent 布局由 ViewPager2 [如问题中所述]).

自动生成

更新

我已经更新了之前的 Github 代码,以便它包含设置 RecyclerViewImplFrameLayout([=49 的 clipChildren 的最终解决方案=]) 在 MainFragment.ktPagerAdapter.kt 中为假。这是 link:link to working example