Android MDC:滚动时ExtendedFloatingActionButton缩放动画丢失位置
问题描述
我实现了一个锚定在BottomNavigationView上的ExtendedFloatingActionButton(EFAB),作为CoordinatorLayout的直接子View,搭配内部的RecyclerView,通过滚动监听实现EFAB的展开/收缩,目标是复刻Material Design 3的滚动交互效果。但遇到问题:快速滚动触发收缩/展开动画时,按钮会被底部导航栏遮挡,仿佛动画过程中丢失了组件的位置锚定关系。禁用缩放动画后问题消失,且未锚定的EFAB不存在此问题,说明是锚定场景下的动画冲突导致。

现有代码
滚动监听类
class FabExtendingOnScrollListener( private val floatingActionButton: ExtendedFloatingActionButton ) : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { if (newState == RecyclerView.SCROLL_STATE_IDLE && !floatingActionButton.isExtended && recyclerView.computeVerticalScrollOffset() == 0 ) { floatingActionButton.extend() } super.onScrollStateChanged(recyclerView, newState) } override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (dy != 0 && floatingActionButton.isExtended) { floatingActionButton.shrink() } super.onScrolled(recyclerView, dx, dy) } }
布局文件
<?xml version="1.0" encoding="utf-8"?> <androidx.drawerlayout.widget.DrawerLayout 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:id="@+id/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:openDrawer="start"> <androidx.coordinatorlayout.widget.CoordinatorLayout android:id="@+id/cord_main_content" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <com.google.android.material.appbar.AppBarLayout android:id="@+id/appBarLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:fitsSystemWindows="true"> <com.google.android.material.appbar.CollapsingToolbarLayout android:id="@+id/collapsingToolbarLayout" app:titleEnabled="false" android:layout_width="match_parent" android:layout_height="wrap_content"> <include android:layout_width="match_parent" android:layout_height="wrap_content" layout="@layout/PartialToolbar"/> <com.google.android.material.progressindicator.LinearProgressIndicator android:id="@+id/topAppBarProgressBar" android:layout_gravity="bottom" android:layout_width="match_parent" android:layout_height="wrap_content" app:hideAnimationBehavior="inward" app:showAnimationBehavior="outward" android:layout_marginTop="?attr/actionBarSize"/> </com.google.android.material.appbar.CollapsingToolbarLayout> </com.google.android.material.appbar.AppBarLayout> <!-- 为让内容位于导航栏上方,我动态添加了底部内边距 --> <FrameLayout android:id="@+id/content_frame_shell" android:layout_width="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" android:layout_height="match_parent"/> <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/fab_shell" app:icon="@drawable/ic_baseline_add_24" app:layout_anchor="@id/bottom_navigation" app:layout_anchorGravity="top|end" android:layout_gravity="top|end" android:layout_margin="@dimen/fab_margin" android:translationY="-16dp"/> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_navigation" android:layout_width="match_parent" android:layout_height="wrap_content" app:elevation="6dp" android:layout_gravity="bottom" android:fitsSystemWindows="true" app:labelVisibilityMode="labeled"/> </androidx.coordinatorlayout.widget.CoordinatorLayout> <com.google.android.material.navigation.NavigationView android:id="@+id/nav_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" android:fitsSystemWindows="true"/> </androidx.drawerlayout.widget.DrawerLayout>
问题原因与解决方法
核心原因
快速滚动时,EFAB的shrink()/extend()动画会修改视图的尺寸和位置参数,而CoordinatorLayout的锚定布局逻辑在动画帧间隙无法及时更新EFAB的位置约束,导致动画结束后EFAB的位置计算错误,被导航栏遮挡。
解决方法
方法1:自定义Behavior强制维护锚定位置
为EFAB实现自定义CoordinatorLayout.Behavior,确保动画过程中始终保持与BottomNavigationView的锚定关系:
class AnchoredExtendedFabBehavior : CoordinatorLayout.Behavior<ExtendedFloatingActionButton>() { override fun layoutDependsOn( parent: CoordinatorLayout, child: ExtendedFloatingActionButton, dependency: View ): Boolean { return dependency is BottomNavigationView } override fun onDependentViewChanged( parent: CoordinatorLayout, child: ExtendedFloatingActionButton, dependency: View ): Boolean { // 强制更新EFAB的位置,确保始终在导航栏上方 val layoutParams = child.layoutParams as CoordinatorLayout.LayoutParams layoutParams.bottomMargin = dependency.height + child.resources.getDimensionPixelSize(R.dimen.fab_margin) child.layoutParams = layoutParams return true } }
在布局中给EFAB添加该Behavior:
app:layout_behavior="com.yourpackage.AnchoredExtendedFabBehavior"
方法2:优化滚动监听,避免动画叠加
在滚动监听中添加防抖判断,避免快速滚动时频繁触发动画:
class FabExtendingOnScrollListener( private val floatingActionButton: ExtendedFloatingActionButton ) : RecyclerView.OnScrollListener() { private var isAnimating = false override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { if (newState == RecyclerView.SCROLL_STATE_IDLE && !floatingActionButton.isExtended && recyclerView.computeVerticalScrollOffset() == 0 && !isAnimating ) { isAnimating = true floatingActionButton.extend(object : ExtendedFloatingActionButton.OnChangedCallback() { override fun onExtensionAnimationEnd(button: ExtendedFloatingActionButton?) { isAnimating = false } }) } super.onScrollStateChanged(recyclerView, newState) } override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { if (dy > 0 && floatingActionButton.isExtended && !isAnimating) { isAnimating = true floatingActionButton.shrink(object : ExtendedFloatingActionButton.OnChangedCallback() { override fun onShrinkAnimationEnd(button: ExtendedFloatingActionButton?) { isAnimating = false } }) } super.onScrolled(recyclerView, dx, dy) } }
通过isAnimating标记避免动画叠加,同时利用动画回调在结束后释放标记,确保每次只有一个动画在执行。
方法3:移除手动translationY干扰
布局中EFAB的android:translationY="-16dp"可能干扰锚定布局的位置计算,尝试移除该属性,改用layout_marginBottom调整与导航栏的间距:
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton ... android:layout_marginBottom="@dimen/fab_margin_extra" android:translationY="0dp"/>
内容的提问来源于stack exchange,提问作者David Marquez
相关产品推荐
相关产品推荐

