You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用ConstraintLayout+Barrier时,RecyclerView最后项因Error Banner显示被截断

Android布局问题:Error Banner显示时RecyclerView最后一项被截断

当分类总权重不等于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高度未自动适配剩余空间,导致被截断。修改点:

  1. 调整RecyclerView的高度属性,让它占据LinearLayout的剩余空间:
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rvGradingCategories"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1" />
  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显示后的剩余空间,避免重叠:

  1. 给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" />
  1. 同样在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 21:15:35