Android实现commentContainer滚动随动并始终显示于底部的需求
实现评论容器随滚动动态固定/跟随的效果
需求说明
我开发了一个长内容Android页面,使用NestedScrollView搭配RecyclerView布局。当前commentContainer位于页面最底部,只有滚动到页面底部才能看到它。需要实现:
- 页面打开时,
commentContainer直接显示在屏幕底部 - 用户滚动页面时,容器随滚动移动,直到
NestedScrollView滚动到commentContainer的原始布局位置后,保持正常布局(即容器不再固定在屏幕底部,而是跟随内容滚动)
解决方案
1. 布局调整
首先修改布局,给NestedScrollView的内部容器添加底部padding(值与commentContainer高度一致),确保滚动到底部时,内容能完全滚动到commentContainer的原始位置下方。同时保持commentContainer初始固定在屏幕底部。
修改后的布局代码:
<?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="match_parent"> <androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:id="@+id/swipeContainer" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginStart="29dp" android:layout_marginEnd="29dp" android:layout_marginBottom="5dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <androidx.core.widget.NestedScrollView android:id="@+id/nestedScrollView" android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/contentContainer" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingBottom="@dimen/comment_container_height"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rvChat" android:layout_width="match_parent" android:layout_height="0dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:itemCount="1" tools:listitem="@layout/item_discussion" /> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/noDataContainer" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="16dp" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <ImageView android:id="@+id/imageView4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_comma_green" app:layout_constraintBottom_toTopOf="@+id/tvNoData" app:layout_constraintEnd_toStartOf="@+id/tvNoData" /> <TextView android:id="@+id/tvNoData" android:layout_width="wrap_content" android:layout_height="wrap_content" android:fontFamily="@font/poppins_regular" android:text="@string/there_is_no_discussion" android:textColor="@color/kafd_color_blue" android:textSize="@dimen/x13sp" android:textStyle="normal" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.core.widget.NestedScrollView> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/commentConatiner" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/shadow_white_bg" android:elevation="10dp" android:visibility="visible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> <EditText android:id="@+id/etComment" android:layout_width="0dp" android:layout_height="50dp" android:layout_marginStart="33dp" android:layout_marginTop="@dimen/x8dp" android:layout_marginEnd="10dp" android:background="@android:color/transparent" android:ems="10" android:fontFamily="@font/poppins_regular" android:gravity="start|center" android:hint="@string/write_here" android:inputType="textMultiLine" android:isScrollContainer="true" android:lineSpacingExtra="3sp" android:scrollbars="vertical" android:textColor="@color/kafd_color_gray_1" android:textColorHint="@color/kafd_color_gray_1" android:textSize="@dimen/x14sp" android:textStyle="normal" app:layout_constraintBottom_toTopOf="@+id/rvSelectedFiles" app:layout_constraintEnd_toStartOf="@+id/ivFile" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ImageView android:id="@+id/ivSend" android:layout_width="wrap_content" android:layout_height="0dp" android:scaleType="fitXY" android:src="@drawable/ic_send" app:layout_constraintBottom_toTopOf="@+id/rvSelectedFiles" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ImageView android:id="@+id/ivFile" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="15dp" android:padding="5dp" android:src="@drawable/ic_add_attachment" app:layout_constraintBottom_toBottomOf="@+id/ivSend" app:layout_constraintEnd_toStartOf="@+id/ivSend" app:layout_constraintTop_toTopOf="@+id/ivSend" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rvSelectedFiles" android:layout_width="0dp" android:layout_height="50dp" android:layout_marginBottom="15dp" android:visibility="gone" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/ivSend" app:layout_constraintStart_toStartOf="@+id/etComment" app:spanCount="2" tools:itemCount="4" tools:listitem="@layout/item_selected_attachment" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
2. 滚动监听逻辑
在Activity/Fragment中,监听NestedScrollView的滚动事件,计算滚动距离,动态切换commentContainer的布局约束:
Kotlin实现
import android.os.Bundle import android.view.ViewTreeObserver import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.widget.NestedScrollView class MainActivity : AppCompatActivity() { private lateinit var nestedScrollView: NestedScrollView private lateinit var commentContainer: ConstraintLayout private lateinit var contentContainer: ConstraintLayout private var commentContainerHeight = 0 private var screenHeight = 0 private var contentHeight = 0 private var triggerScrollDistance = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) nestedScrollView = findViewById(R.id.nestedScrollView) commentContainer = findViewById(R.id.commentConatiner) contentContainer = findViewById(R.id.contentContainer) // 等待视图绘制完成后计算高度 contentContainer.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { contentContainer.viewTreeObserver.removeOnGlobalLayoutListener(this) commentContainerHeight = commentContainer.height screenHeight = resources.displayMetrics.heightPixels contentHeight = contentContainer.height // 计算触发切换的滚动距离:内容高度 - 屏幕可见高度 + 评论容器高度 triggerScrollDistance = contentHeight - screenHeight + commentContainerHeight nestedScrollView.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, _ -> val params = commentContainer.layoutParams as ConstraintLayout.LayoutParams if (scrollY >= triggerScrollDistance) { // 滚动到足够距离,评论容器跟随内容底部 params.bottomToBottom = ConstraintLayout.LayoutParams.UNSET params.topToBottom = R.id.contentContainer } else { // 未达到触发距离,评论容器固定在屏幕底部 params.topToBottom = ConstraintLayout.LayoutParams.UNSET params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID } commentContainer.layoutParams = params }) } }) } }
Java实现
import android.os.Bundle; import android.view.ViewTreeObserver; import androidx.appcompat.app.AppCompatActivity; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.widget.NestedScrollView; public class MainActivity extends AppCompatActivity { private NestedScrollView nestedScrollView; private ConstraintLayout commentContainer; private ConstraintLayout contentContainer; private int commentContainerHeight; private int screenHeight; private int contentHeight; private int triggerScrollDistance; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); nestedScrollView = findViewById(R.id.nestedScrollView); commentContainer = findViewById(R.id.commentConatiner); contentContainer = findViewById(R.id.contentContainer); contentContainer.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { contentContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); commentContainerHeight = commentContainer.getHeight(); screenHeight = getResources().getDisplayMetrics().heightPixels; contentHeight = contentContainer.getHeight(); triggerScrollDistance = contentHeight - screenHeight + commentContainerHeight; nestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() { @Override public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) commentContainer.getLayoutParams(); if (scrollY >= triggerScrollDistance) { params.bottomToBottom = ConstraintLayout.LayoutParams.UNSET; params.topToBottom = R.id.contentContainer; } else { params.topToBottom = ConstraintLayout.LayoutParams.UNSET; params.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; } commentContainer.setLayoutParams(params); } }); } }); } }
关键说明
- 布局中给
contentContainer添加的paddingBottom必须等于commentContainer的高度,确保滚动到底部时,内容能完全覆盖到评论容器的原始位置下方 triggerScrollDistance的计算逻辑:当滚动距离达到这个值时,评论容器的顶部刚好对齐内容容器的底部,此时切换约束让它跟随内容滚动- 使用
ViewTreeObserver等待视图绘制完成后再计算高度,避免获取到0值
内容的提问来源于stack exchange,提问作者Ahmed Elsayed
相关产品推荐
相关产品推荐

