使用ConstraintLayout+Barrier时,RecyclerView最后项因Error Banner显示被截断
当分类总权重不等于100时,Error Banner会显示,但此时RecyclerView的最后一项会被截断,截断高度恰好等于Error Banner的高度。只有再次编辑并弹出键盘后,RecyclerView的高度才会刷新恢复正常。
当前布局采用ConstraintLayout作为父布局,内部通过LinearLayoutCompat包裹Error Banner和RecyclerView,同时使用Barrier关联下方SwitchMaterial的位置,并动态设置layoutGradeCategoryList的最大高度为设备可用高度的70%。尝试将外层LinearLayout改为ConstraintLayout后,又出现Error Banner与RecyclerView重叠的问题。
初始布局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>
动态设置最大高度的Kotlin代码
// 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)
控制Error Banner显示的Kotlin代码
val result = sum == 100 binding.apply { buttonSave.isEnabled = result tvInvalidGradeWeightLayout.isVisible = !result }
修改后的布局XML(改为ConstraintLayout后出现重叠)
<?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>
解决方案
方案1:修复初始LinearLayoutCompat布局
初始布局中,LinearLayoutCompat的总高度会随Error Banner显示而增加,但RecyclerView的wrap_content高度未自动适配剩余空间,导致被截断。修改点:
- 调整RecyclerView的高度属性,让它占据LinearLayout的剩余空间:
<androidx.recyclerview.widget.RecyclerView android:id="@+id/rvGradingCategories" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" />
- 在Error Banner可见性变化后,手动触发布局重计算:
val result = sum == 100 binding.apply { buttonSave.isEnabled = result tvInvalidGradeWeightLayout.isVisible = !result // 触发父容器重新计算高度 layoutGradeCategoryList.requestLayout() // 通知RecyclerView刷新布局 rvGradingCategories.post { rvGradingCategories.adapter?.notifyDataSetChanged() } }
方案2:修复修改后的ConstraintLayout布局(解决重叠)
修改后的内层ConstraintLayout需要让RecyclerView正确适配Error Banner显示后的剩余空间,避免重叠:
- 给RecyclerView添加底部约束,并设置高度为0dp以填充剩余空间:
<androidx.recyclerview.widget.RecyclerView android:id="@+id/rvGradingCategories" android:layout_width="match_parent" android:layout_height="0dp" app:layout_constrainedHeight="true" app:layout_constraintTop_toBottomOf="@+id/tvInvalidGradeWeightLayout" app:layout_constraintBottom_toBottomOf="parent" />
- 同样在Error Banner可见性变化后触发布局刷新:
val result = sum == 100 binding.apply { buttonSave.isEnabled = result tvInvalidGradeWeightLayout.isVisible = !result layoutGradeCategoryList.requestLayout() }
核心原因
动态修改View可见性时,ConstraintLayout/LinearLayout不会自动强制重新计算所有子View的布局参数,需要手动调用requestLayout()触发重绘。同时,给RecyclerView设置固定的填充模式(0dp+weight/约束),能避免wrap_content带来的高度计算偏差。
内容的提问来源于stack exchange,提问作者heychar

