1

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.

Problem


Update

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.

Layout Inspector

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.

Success

Failure

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.


Update 2

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.

Richard
  • 7,037
  • 2
  • 23
  • 76
  • from bottom is it being cutoff.? – Atif AbbAsi Apr 23 '20 at 10:11
  • @AtifAbbAsi What does from bottom mean? If you mean the shadow, then no. It's being cut off from the left, right, and top side. – Richard Apr 23 '20 at 10:12
  • please attach reference image.@Richard – Atif AbbAsi Apr 23 '20 at 10:14
  • try adding margin .? – Atif AbbAsi Apr 23 '20 at 10:54
  • @AtifAbbAsi My layout is already structured well. I know that adding that would solve the problem, but it introduces a new problem: the layout of the item view is not aligned on the left and right side of other elements on the page. – Richard Apr 23 '20 at 11:34
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/212342/discussion-between-atif-abbasi-and-richard). – Atif AbbAsi Apr 23 '20 at 12:02
  • Can someone help me with this? – Richard May 05 '20 at 04:42
  • ^ this is not the reason, but I'd not merge a ConstraintLayout XML with `match_parent`. It should be `match_constrains` or `0dp` – Martin Marconcini May 06 '20 at 12:31
  • @MartinMarconcini I'm not quite sure which ConstraintLayout you are referring to. Also, you are correct. The issue is not on the layout width. – Richard May 06 '20 at 12:33
  • @Richard none, the above poster didn't read before commenting a useless and incorrect assertion. It appears your Layouts use `match_parent` only when they are not part of a ConstraintLayout parent (which is absolutely correct). I'm interested in hearing about this: `but it introduces a new problem: the layout of the item view is not aligned on the left and right side of other elements on the page.` -> this is common, and more often than not, you have to use padding/margin to compensate for this offsetting. – Martin Marconcini May 06 '20 at 13:17
  • @MartinMarconcini I meant that I would need to adjust all my paddings/margins (to avoid clipping) of other related elements to align them properly. However, for now, the technique above (setting all direct and indirect parents' `clipChildren` to false) has always worked for all my other layouts. The problem only appears when ViewPager2 is used because it automatically introduces that FrameLayout, which has a `clipChildren` attribute that I can't change. In the end, if nothing works, I can always fall back to changing padding/margin, but is there a way to avoid that? – Richard May 06 '20 at 13:36
  • @Richard I don't know off of the top of my head. I was looking at the current app I'm working on (where we incidentally have a VP2 with a list in it), and I don't see any _special_ handling on it that could be related to the layout parameters. :/ – Martin Marconcini May 06 '20 at 14:45
  • @Richard I usually offer some time to SO in this chat channel, so feel free to drop by if you wanna talk about it in more detail. If I'm not there, ping me, I may show up (but note that I do it when I can, which is not always/every day). https://chat.stackoverflow.com/rooms/210228/android-help – Martin Marconcini May 06 '20 at 14:48
  • @MartinMarconcini Does your current app have an item inside a ViewPager2 with its width from the start to the end edge of the ViewPager2 with its shadow being drawn by the `elevation` property? If you do and if its shadow is not clipped, then it's concerning why mine is misbehaving. I'll try to add a reproducible example with a simple ViewPager2 layout later. I'll ping you in the chat then ;-) Thanks. – Richard May 06 '20 at 15:35
  • @MartinMarconcini I've also added the app which shows the problem (in Github). Running it and using the layout inspector should suffice to see what I'm seeing. I hope that you have the time to take a look at it and help me with the problem ;-) – Richard May 06 '20 at 17:30

3 Answers3

5

After looking at your problem in more detail, I believe the issue is the ViewHolder XML using a Drawable as a background trying to simulate the Drop shadows.

Turns out that shadows on Android are not "real shadows" but fake drawn elements by the framework. These shadows use -in some cases- extra padding added by the framework; again.

CardViews, have an internal mechanism to deal with this (Especially on platforms where there was no concept of elevation).

For this reason, I recommend you change your ViewHOlder to contain a CardView. In which case it would look like:

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

And then... remove all the clipChildren/CliptoPadding, etc. Those would cause you more headaches than solutions and have a performance penalty (the framework now needs to draw extra stuff that would otherwise not be rendered because it's covered).

I hope this helps you.

Regarding the FrameLayout, that's a VP "thing" and I hope Google replaces that with the new "FragmentContainer" in latest AppCompat implementations.

Best of luck.

Here's how it looks now:

enter image description here

Martin Marconcini
  • 26,875
  • 19
  • 106
  • 144
  • Here's a caveat of using `app:cardUseCompatPadding`: it causes padding to be added to the CardView (to draw the shadow). This will be problematic when your shadow is large (e.g. caused by an elevation of 32dp). However, if you're designing for Android L and above, `app:cardUseCompatPadding` can be avoided and the extra, perhaps unwanted, padding can be removed while the shadow remains properly drawn. – Richard May 08 '20 at 02:10
  • Yes, this is a side-effect of how Shadows were implemented and how they had to deal with "what about the platforms that have no idea what shadow is?". Unfortunately, the way it was implemented caused a lot of headaches and SO questions about shadows not working, etc. In the end, the "compatPadding" is a (badly documented/bad idea) that stuck and here we are. :) – Martin Marconcini May 08 '20 at 09:16
  • Use "app:cardUseCompatPadding="true"" will work – Thomas M K Jul 24 '23 at 01:55
3

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.


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

Setting the clipChildren attribute to false

As 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]).


Update

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

Richard
  • 7,037
  • 2
  • 23
  • 76
-1

add clipToPadding="false" to the root ConstraintLayout in the xml file inflated for the ViewHolder.

Ismail Shaikh
  • 482
  • 4
  • 13