ViewPager2 中项目的阴影被剪裁
Item's shadow inside a ViewPager2 is clipped
我有一个 activity 有一个 NavHostFragment
。此 NavHostFragment
将承载三个片段,其中两个是 FragmentA
和 FragmentB
。在 FragmentB
中,我有一个 ViewPager2
,它有两个页面:PageA
和 PageB
,它们实际上都是从一个片段 FragmentC
构建的。在每个 PageA
和 PageB
中,我有一个 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
。我还将 ViewPager2
的 clipChildren
设置为 false
。这是 PageA
和 PageB
的布局 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
即使 ViewPager2
的 clipChildren
设置为 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。此 FrameLayout
的 clipChildren
设置为 true。我很确定我必须将 FrameLayout
的 clipChildren
设置为 false
以使阴影不被剪裁,但我可能错了。
以下是我成功将 RecyclerViewImpl
的 clipChildren
设置为 false
而未能将 FrameLayout
的 clipChildren
设置为 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 有问题的 FrameLayout
的 clipChildren
定位为 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
如果您使用上面的依赖项,您可以看到 FrameLayout
在 FragmentStateAdapter
class 的 onCreateViewHolder
函数中创建(第 160 行)。
@NonNull
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return FragmentViewHolder.create(parent);
}
FragmentViewHolder.create(parent)
方法将始终创建一个 FrameLayout
视图持有者。这将在 FragmentStateAdapter
的 onBindViewHolder
中作为参数 (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
将作为参数传递到 FragmentStateAdapter
的 onBindViewHolder
方法中,我们可以覆盖 onBindViewHolder
(在您自己的适配器中class 扩展了 FragmentStateAdapter
),将 holder.itemView
的 clipChildren
属性设置为 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 代码,以便它包含设置 RecyclerViewImpl
和 FrameLayout
([=49 的 clipChildren
的最终解决方案=]) 在 MainFragment.kt
和 PagerAdapter.kt
中为假。这是 link:link to working example
我有一个 activity 有一个 NavHostFragment
。此 NavHostFragment
将承载三个片段,其中两个是 FragmentA
和 FragmentB
。在 FragmentB
中,我有一个 ViewPager2
,它有两个页面:PageA
和 PageB
,它们实际上都是从一个片段 FragmentC
构建的。在每个 PageA
和 PageB
中,我有一个 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
。我还将 ViewPager2
的 clipChildren
设置为 false
。这是 PageA
和 PageB
的布局 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
即使 ViewPager2
的 clipChildren
设置为 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。此 FrameLayout
的 clipChildren
设置为 true。我很确定我必须将 FrameLayout
的 clipChildren
设置为 false
以使阴影不被剪裁,但我可能错了。
以下是我成功将 RecyclerViewImpl
的 clipChildren
设置为 false
而未能将 FrameLayout
的 clipChildren
设置为 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 有问题的 FrameLayout
的 clipChildren
定位为 false;从而取消项目的阴影。如果您不介意修改当前的 XML 布局,请查看 CardView
绘制未剪裁的阴影)也可以解决此问题。
正如所怀疑的那样,提到的 FrameLayout
是导致 children 被剪裁的原因。 FrameLayout
由扩展 class RecyclerView.Adapter<FragmentViewHolder>
的 class FragmentStateAdapter
创建。根据我收集到的信息,这个 FragmentViewHolder
基本上与普通 RecyclerView
的视图持有者类似。 FragmentViewHolder
的 FrameLayout
(即 ViewGroup
)。 view holder 将始终 return a ViewGroup
这一事实似乎即使在未来也不太可能改变。
依赖关系androidx.viewpager2:viewpager2:1.0.0
如果您使用上面的依赖项,您可以看到 FrameLayout
在 FragmentStateAdapter
class 的 onCreateViewHolder
函数中创建(第 160 行)。
@NonNull
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return FragmentViewHolder.create(parent);
}
FragmentViewHolder.create(parent)
方法将始终创建一个 FrameLayout
视图持有者。这将在 FragmentStateAdapter
的 onBindViewHolder
中作为参数 (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
将作为参数传递到 FragmentStateAdapter
的 onBindViewHolder
方法中,我们可以覆盖 onBindViewHolder
(在您自己的适配器中class 扩展了 FragmentStateAdapter
),将 holder.itemView
的 clipChildren
属性设置为 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 代码,以便它包含设置 RecyclerViewImpl
和 FrameLayout
([=49 的 clipChildren
的最终解决方案=]) 在 MainFragment.kt
和 PagerAdapter.kt
中为假。这是 link:link to working example