I have an activity that has a NavHostFragment
. This NavHostFragment
will host three fragments, two of which are FragmentA
and FragmentB
. Inside FragmentB
, I have a ViewPager2
which has two pages: PageA
and PageB
, both are actually constructed from one fragment, FragmentC
. Inside each PageA
and PageB
, I have one RecyclerView
.
Here's the layout XML for FragmentB
.
<?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>
As you can see, I've set the fragment's root layout's clipChildren
to false
. I've also set the ViewPager2
's clipChildren
to false
. Here's the layout XML for PageA
and PageB
(i.e. 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>
As you can see, it only has RecyclerView
and I've set clipChildren
to false
. The inflated XML layout for the ViewHolder of the RV is the following.
<?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>
The clipped portion is the shadow from the elevation
of the above ViewHolder
's XML layout. As I've set all the clipChildren
attributes from all parents to false
, the shadow shouldn't have been clipped, yet it still is. Why is this happening? How can I prevent it from being clipped without changing the padding/margin?
Note: I also have a RecyclerView
inside FragmentA
, but the difference is that the RecyclerView
inside FragmentA
is not nested within a ViewPager2
. Following the methods (setting all the parents' clipChildren
to false
) on FragmentA
allows the RecyclerView
's items to show their shadows.
Here's the image of the problem.
Using the Layout Inspector, it seems like inside ViewPager2
, there are more ViewGroups (marked with the red rectangle). My RecyclerView
with its items clipped is marked with the green rectangle. Here's what the Layout Inspector shows.
As can be seen, inside ViewPager2
, there is a ViewPager2$RecyclerViewImpl
and inside it, there's a FrameLayout
(I did not create these ViewGroups). It turns out that these two have clipChildren
set to true
even when the ViewPager2
's clipChildren
is set to false. I can target the ViewPager2$RecyclerViewImpl
inside my FragmentB
like so.
(viewPager.getChildAt(0) as ViewGroup).clipChildren = false
I then tried targetting the FrameLayout
using a similar method.
((viewPager.getChildAt(0) as ViewGroup).getChildAt(0) as ViewGroup).clipChildren = false
However, I got an error saying that the second getChildAt(0)
returns null
. In my Layout Inspector, it clearly shows that there's a FrameLayout before my RecyclerView. This FrameLayout
has its clipChildren
set to true. I'm pretty sure that I have to set the FrameLayout
's clipChildren
to false
in order for shadows to not be clipped, but I may be wrong.
Here are screenshots showing that I managed to set the clipChildren
of RecyclerViewImpl
to false
and failed to set the clipChildren
of FrameLayout
to false
respectively.
Or is there a better way to unclip the shadows?
I obscured the layout preview for private reason; this causes the layout preview to be a white box.
For those who would like to view the problem directly, simply run the app that I provided in this Github link. Use the Layout Inspector to see what I'm seeing.
This answer directly explains how we can target the clipChildren
of the auto-generated problematic FrameLayout
to false; thus unclipping the items' shadows . If you don't mind modifying your current XML layout, check out @Martin Marconcini's equally awesome answer (using a CardView
to draw the shadow that is not clipped) to also solve this problem.
As suspected, the mentioned FrameLayout
is what's causing the children to be clipped. The FrameLayout
is created by the class FragmentStateAdapter
that extends the class RecyclerView.Adapter<FragmentViewHolder>
. From what I've gathered, this FragmentViewHolder
basically works similar to the normal RecyclerView
's view holder. This FragmentViewHolder
's itemView
, for now, always returns a FrameLayout
(which is a ViewGroup
). The fact that the view holder will always return a ViewGroup
seems unlikely to change even in the future.
androidx.viewpager2:viewpager2:1.0.0
If you're using the dependency above, you can see the FrameLayout
being created inside the onCreateViewHolder
function of the FragmentStateAdapter
class (line 160).
@NonNull
@Override
public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return FragmentViewHolder.create(parent);
}
The FragmentViewHolder.create(parent)
method will always create a FrameLayout
view holder. This will be passed as a parameter (holder
) in the onBindViewHolder
of FragmentStateAdapter
. Method declaration for FragmentViewHolder.create
is as follows.
@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
attribute to falseAs we now know that the holder
will be passed as a parameter in the onBindViewHolder
method of FragmentStateAdapter
, we can override onBindViewHolder
(inside your own adapter class that extends FragmentStateAdapter
), set the holder.itemView
's clipChildren
attribute to false
and voila, the item inside the RecyclerView will no longer be clipped.
The final code to my adapter class is as follows.
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)
}
}
Other parent layouts above the hierarchy that have bounding boxes smaller than the needed area for the shadow also need their clipChildren
set to false
(namely, the RecyclerViewImpl
parent layout generated automatically by ViewPager2
[as mentioned in the question]).
I've updated the previous Github code so that it includes the final solution to set clipChildren
of both RecyclerViewImpl
and FrameLayout
(of the ViewPager2
) to false in MainFragment.kt
and PagerAdapter.kt
. Here's the link: link to working example