Negative Margins For RecyclerView Item Decoration

1k views Asked by At

I need to implement the below layout for my RecyclerView Items (The picture represents two rows):

enter image description here

This is my XML file:

<?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:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/theme_primary_color">

    <View
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:background="@drawable/half_circle"
        android:translationZ="3dp"
        app:layout_constraintBottom_toTopOf="@id/cvTop"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@id/cvTop" />

    <androidx.cardview.widget.CardView
        android:id="@+id/cvTop"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="2dp"
        android:layout_marginEnd="8dp"
        app:cardCornerRadius="@dimen/card_view_corner"
        app:layout_constraintTop_toTopOf="parent">

    </androidx.cardview.widget.CardView>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fabCall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_call"
        app:backgroundTint="@color/fab_green"
        app:fabSize="mini"
        app:layout_constraintBottom_toBottomOf="@+id/cvTop"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/cvTop" />

</androidx.constraintlayout.widget.ConstraintLayout>

I tried to set negative margins to root ConstraintLayout but the second item went to top of the first one, but I need the first item to be on top of the second one.

2

There are 2 answers

5
Zain On BEST ANSWER

So, this is not exactly what you want but at least it has the same layout as you want with a simpler approach.

So, the main challenges are:

  • Taking a curve cutout on the CardView probably need to be built programmatically with canvas for a better result. But for simplicity, this is replaced by a BottomAppBar wrapped in a CoordinatorLayout in order to have the curve effect with the top circle/gap.

  • Replacing the top View with Fab in order to have an Inset FAB by setting the layout_anchor to the BottomAppBar. Check material design for this.

    And having the cutout behavior requires to make the FAB like it doesn't exist by setting a transparent backgroundTint & removing the outlineProvider

  • Making the top cutout (gap) of a particular row get overlapped to the top row like if it is a part of it. This works with the negative margin on the root view.

<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="-20dp"
    android:background="@android:color/transparent">


    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/cvTop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/transparent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:outlineProvider="none"
            app:backgroundTint="@android:color/transparent"
            app:layout_anchor="@id/navigation" />

        <com.google.android.material.bottomappbar.BottomAppBar
            android:id="@+id/navigation"
            android:layout_width="match_parent"
            android:layout_height="150dp"
            android:layout_gravity="bottom"
            android:layout_marginStart="8dp"
            android:layout_marginTop="2dp"
            android:layout_marginEnd="8dp"
            app:backgroundTint="@android:color/holo_blue_dark">

        </com.google.android.material.bottomappbar.BottomAppBar>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fabCall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:backgroundTint="@color/fab_green"
        app:layout_constraintBottom_toBottomOf="@+id/cvTop"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/cvTop" />

    <TextView
        android:id="@+id/tv_item_num"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/white"
        android:textSize="18sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="Item No. 0" />

</androidx.constraintlayout.widget.ConstraintLayout>

Note: you can remove the TextView, I just added it to check the item doesn't change its position like you had in your case.

Preview on the RecyclerView

enter image description here

UPDATE:

A new challenge:

  • Only the top half of the FAB will intercept touch events, as any particular row will be laid on top of its direct top row; and hence the FAB of the top row won't intercept events in the intersection area.

    Well, this can be manipulated well using canvas & custom View. But also this can be solved in the current approach by laying out the rows of the RecyclerViews from bottom to top. One way to setReverseLayout(true) and then either:

    => Reverse the RecyclerView list positions before submitting to the adapter

    => Or use mList.size - 1 - position instead of position within the adapter. Assuming the list of items is mList

0
Cheticamp On

I assume that the general RecyclerView layout looks and operates OK except that each item view overlays part of the item view above such that the bottom part of the top view's FAB cannot be seen.

I believe that what you are seeing is a result of the top-down default drawing order of RecyclerView. To change the drawing order to bottom-up so the bottom-most view overlays the one above, take a look at RecyclerView.ChildDrawingOrderCallback to see if you can get a reverse drawing order that will work.

Try the following:

mRecycler.setChildDrawingOrderCallback(new RecyclerView.ChildDrawingOrderCallback() {
    @Override
    public int onGetChildDrawingOrder(int childCount, int i) {
        return childCount - i - 1;
    }
});

I am not sure how you are offsetting your items, but consider using an RecyclerView.ItemDecoration. Something like this:

class OverlapItemDecoration(context: Context, overlapDp: Int) : RecyclerView.ItemDecoration() {

    private val overlapPx: Int

    init {
        overlapPx = (context.resources.displayMetrics.density * overlapDp.toFloat()).toInt()
    }

    override fun getItemOffsets(
        outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State
    ) {
        if (parent.getChildAdapterPosition(view) == 0) return
        outRect.set(0, overlapPx, 0, 0)
    }
}

Both of these techniques, together, will create a RecyclerView with overlapping item views laid out bottom to top so the FAB is 100% clickable.

Here is an example of the application of these techniques. I also replaced the CardView with a MaterialCardView with a ShapeableImageView as a background.

enter image description here