【问题标题】:Item's shadow inside a ViewPager2 is clippedViewPager2 内的项目阴影被剪裁
【发布时间】:2020-04-23 10:03:44
【问题描述】:

我有一个具有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(即FragmentC)的布局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">

    <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 的阴影。因为我已经将所有父母的所有clipChildren 属性设置为false,所以不应该剪掉阴影,但它仍然是。为什么会这样?如何在不更改填充/边距的情况下防止它被剪裁?

注意: 我在FragmentA 中也有一个RecyclerView,但不同的是FragmentA 中的RecyclerView 没有嵌套在ViewPager2 中。按照FragmentA上的方法(将所有父母的clipChildren设置为false)允许RecyclerView的项目显示阴影。

这是问题的图像。


更新

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

可以看出,在ViewPager2 内部有一个ViewPager2$RecyclerViewImpl,在其中有一个FrameLayout(我没有创建这些视图组)。事实证明,即使ViewPager2clipChildren 设置为false,这两个clipChildren 设置为true。我可以像这样在FragmentB 中定位ViewPager2$RecyclerViewImpl

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

然后我尝试使用类似的方法定位FrameLayout

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

但是,我收到一条错误消息,提示第二个 getChildAt(0) 返回 null。在我的 Layout Inspector 中,它清楚地表明在我的 RecyclerView 之前有一个 FrameLayout。这个FrameLayoutclipChildren 设置为true。我很确定我必须将FrameLayoutclipChildren 设置为false 才能不剪裁阴影,但我可能错了。

这里的屏幕截图显示我设法将RecyclerViewImplclipChildren 设置为false,但未能分别将FrameLayoutclipChildren 设置为false

或者有没有更好的方法来解开阴影?

我出于私人原因隐藏了布局预览;这会导致布局预览变成一个白框。


更新 2

对于那些想直接查看问题的人,只需运行我在此Github link 中提供的应用程序。使用 Layout Inspector 查看我看到的内容。

【问题讨论】:

  • 自下而上是否被截断。?
  • @AtifAbbAsi 从底部是什么意思?如果你的意思是影子,那么没有。它从左侧、右侧和顶部被切断。
  • 请附上参考图片。@Richard
  • 尝试添加边距。?
  • @AtifAbbAsi 我的布局已经结构良好。我知道添加它可以解决问题,但它引入了一个新问题:项目视图的布局与页面上其他元素的左侧和右侧不对齐。

标签: android android-layout android-recyclerview android-view android-viewpager2


【解决方案1】:

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

事实证明,Android 上的阴影不是“真正的阴影”,而是框架绘制的假元素。这些阴影在某些情况下使用框架添加的额外填充;再次。

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>

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

希望对你有帮助。

关于 FrameLayout,这是一个 VP“东西”,我希望 Google 在最新的 AppCompat 实现中用新的“FragmentContainer”替换它。

祝你好运。

现在是这样的:

【讨论】:

  • 这里有一个使用app:cardUseCompatPadding的警告:它会导致向 CardView 添加填充(以绘制阴影)。当您的阴影很大时(例如由 32dp 的高度引起),这将是一个问题。但是,如果您正在为 Android L 及更高版本进行设计,则可以避免使用 app:cardUseCompatPadding,并且可以在阴影保持正确绘制的同时移除额外的(可能是不需要的)填充。
  • 是的,这是 Shadows 的实现方式以及它们必须如何处理“不知道什么是 shadow 的平台怎么办?”的副作用。不幸的是,它的实现方式引起了很多令人头疼的问题和关于阴影不起作用等的问题。最后,“compatPadding”是一个(记录不充分/坏主意)卡住了,我们在这里。 :)
【解决方案2】:

这个答案直接解释了我们如何将自动生成的有问题的FrameLayoutclipChildren 定位为false;从而解开项目的阴影。如果您不介意修改当前的 XML 布局,请查看@Martin Marconcini's equally awesome answer(使用CardView 来绘制未剪裁的阴影)也可以解决此问题。


正如怀疑的那样,提到的FrameLayout 是导致孩子被剪掉的原因。 FrameLayout 是由扩展类 RecyclerView.Adapter&lt;FragmentViewHolder&gt; 的类 FragmentStateAdapter 创建的。从我收集到的信息来看,这个FragmentViewHolder 基本上与普通RecyclerView 的视图持有者类似。这个FragmentViewHolderitemView 目前总是返回一个FrameLayout(这是一个ViewGroup)。视图持有者将始终返回 ViewGroup 这一事实似乎即使在未来也不会改变。


依赖androidx.viewpager2:viewpager2:1.0.0

如果您使用上述依赖项,您可以看到在 FragmentStateAdapter 类的 onCreateViewHolder 函数中创建了 FrameLayout(第 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(在您自己的扩展FragmentStateAdapter 的适配器类中),设置holder.itemViewclipChildren 属性为 false 和瞧,RecyclerView 内的项目将不再被剪辑。

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

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

层次结构之上的边界框小于阴影所需区域的其他父布局也需要将其clipChildren设置为false(即ViewPager2自动生成的RecyclerViewImpl父布局[如前所述在问题中])。


更新

我已经更新了之前的 Github 代码,使其包含在 MainFragment.ktPagerAdapter.kt 中将 RecyclerViewImplFrameLayoutViewPager2)的 clipChildren 设置为 false 的最终解决方案。这是链接:link to working example

【讨论】:

    【解决方案3】:

    clipToPadding="false" 添加到为ViewHolder 膨胀的xml 文件中的根ConstraintLayout

    【讨论】:

      猜你喜欢
      • 2013-07-12
      • 1970-01-01
      • 1970-01-01
      • 2018-07-28
      • 1970-01-01
      • 2023-03-27
      • 2019-05-17
      • 1970-01-01
      • 2022-10-07
      相关资源
      最近更新 更多