Search code examples
androidandroid-recyclerviewandroid-constraintlayoutbarrier

RecyclerView's last item is cut off when TextView Error Banner is visible. I'm using Constraint Layout with Barrier


I'm not sure how to ask this question. It seems easy to do but I'm not sure why it's not working. Anyways, here goes. I have this Error Banner that should be visible only if the total "Weight" for the Categories is less than or greater than 100. However my issue is that when the error banner is visible, my RecyclerView's last item is cut off, I think by the height of the error banner. It will only be fixed if I edit it once again and the keyboard is shown, the recyclerview's height is refreshed (probably).

Here's my Layout 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"
    tools:context=".presentation.screens.gradesetup.gradingcategories.GradingCategoriesFragment">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/layoutParent"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.appcompat.widget.LinearLayoutCompat
            android:id="@+id/layoutGradeCategoryList"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constrainedHeight="true" >

            <TextView
                android:id="@+id/tvInvalidGradeWeightLayout"
                style="@style/Default_Text_Error_Banner"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="@dimen/default_margin_size_16dp"
                android:text="@string/error_invalid_grade_category_weight"
                android:visibility="gone" />

            <!-- Constraint layout_constraintHeight_max dynamically to 24% -->
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rvGradingCategories"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />

        </androidx.appcompat.widget.LinearLayoutCompat>

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/tvInfoNoData"
            style="@style/Default_Text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="@string/text_no_data_display"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.constraintlayout.widget.Barrier
            android:id="@+id/barrier"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:barrierDirection="bottom"
            app:constraint_referenced_ids="layoutGradeCategoryList" />

        <androidx.appcompat.widget.LinearLayoutCompat
            android:id="@+id/layout_adjust_weight"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/default_margin_size_16dp"
            android:orientation="horizontal"
            app:layout_constraintBottom_toTopOf="@+id/buttonSave"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/barrier"
            app:layout_constraintVertical_bias="0.01">

            <com.google.android.material.switchmaterial.SwitchMaterial
                android:id="@+id/switchViewAdjustWeight"
                style="@style/Default_Text_Bold"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentEnd="true"
                android:layout_centerVertical="true"
                android:layout_marginEnd="@dimen/screen_padding_size"
                android:text="@string/text_adjust_weight"
                android:theme="@style/Theme.LMS"
                android:visibility="gone" />

        </androidx.appcompat.widget.LinearLayoutCompat>

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/default_margin_size_30dp"
            android:src="@drawable/ic_plus"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:tint="@color/white" />

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/buttonSave"
            style="@style/Primary_Button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/default_margin_size_30dp"
            android:enabled="false"
            android:text="@string/text_save"
            android:visibility="gone"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

As you can see I also have this Constraint Barrier, so whenever the height of the RecyclerView is longer, the SwitchMaterial("switchViewAdjustWeight") is also adjusted. I set the android:layout_constraintHeight_max dynamically based on device height.

// Dynamically set constraintMaxHeight
var constraintMaxHeight = 0
constraintMaxHeight =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        val windowMetrics = activity!!.windowManager.currentWindowMetrics
        val insets: Insets = windowMetrics.windowInsets
            .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
        val height = windowMetrics.bounds.height() - insets.top - insets.bottom
        (height * 0.7).toInt()
    } else {
        val displayMetrics = DisplayMetrics()
        activity!!.windowManager.defaultDisplay.getMetrics(displayMetrics)
        (displayMetrics.heightPixels * 0.7).toInt()
    }

val constraintSet = ConstraintSet()
constraintSet.clone(layoutParent)
constraintSet.constrainMaxHeight(R.id.layoutGradeCategoryList, constraintMaxHeight)
constraintSet.applyTo(layoutParent)

In my code (kotlin): the error banner is toggled to visible if sum != 100

val result = sum == 100
binding.apply {
     buttonSave.isEnabled = result
     tvInvalidGradeWeightLayout.isVisible = !result
}

enter image description here

enter image description here

Target UI:

enter image description here

I also did change LinearLayout to ConstraintLayout However it didn't work, the Banner overlaps with the RecyclerView list :

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

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/layoutParent"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/layoutGradeCategoryList"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constrainedHeight="true" >

            <TextView
                android:id="@+id/tvInvalidGradeWeightLayout"
                style="@style/Default_Text_Error_Banner"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="@dimen/default_margin_size_16dp"
                android:text="@string/error_invalid_grade_category_weight"
                android:visibility="gone"
                app:layout_constraintTop_toTopOf="parent"/>

            <!-- Constraint layout_constraintHeight_max dynamically to 24% -->
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rvGradingCategories"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_constrainedHeight="true"
                app:layout_constraintTop_toBottomOf="@+id/tvInvalidGradeWeightLayout" />

        </androidx.constraintlayout.widget.ConstraintLayout>

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/tvInfoNoData"
            style="@style/Default_Text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="@string/text_no_data_display"
            android:visibility="visible"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <androidx.constraintlayout.widget.Barrier
            android:id="@+id/barrier"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:barrierDirection="bottom"
            app:constraint_referenced_ids="layoutGradeCategoryList" />

        <androidx.appcompat.widget.LinearLayoutCompat
            android:id="@+id/layout_adjust_weight"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/default_margin_size_16dp"
            android:orientation="horizontal"
            app:layout_constraintBottom_toTopOf="@+id/buttonSave"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/barrier"
            app:layout_constraintVertical_bias="0.01">

            <com.google.android.material.switchmaterial.SwitchMaterial
                android:id="@+id/switchViewAdjustWeight"
                style="@style/Default_Text_Bold"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentEnd="true"
                android:layout_centerVertical="true"
                android:layout_marginEnd="@dimen/screen_padding_size"
                android:text="@string/text_adjust_weight"
                android:theme="@style/Theme.LMS"
                android:visibility="gone" />

        </androidx.appcompat.widget.LinearLayoutCompat>

        <com.google.android.material.floatingactionbutton.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/default_margin_size_30dp"
            android:src="@drawable/ic_plus"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:tint="@color/white" />

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/buttonSave"
            style="@style/Primary_Button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="@dimen/default_margin_size_30dp"
            android:enabled="false"
            android:text="@string/text_save"
            android:visibility="gone"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

Solution

  • This worked for me, I added getViewTreeObserver().addOnGlobalLayoutListener for layoutGradeCategoryList and trigger the request layout there.

    layoutGradeCategoryList.getViewTreeObserver().addOnGlobalLayoutListener(OnGlobalLayoutListener {
        layoutGradeCategoryList.requestLayout();
    })
    

    This helped me understand more on requestLayout/forceLayout/invalidate view. https://stackoverflow.com/a/42430695/3777452

    Hope this helps someone else.