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

Android MDC:滚动时ExtendedFloatingActionButton缩放动画丢失位置

锚定BottomNavigationView的ExtendedFAB快速滚动时被遮挡的问题解决

问题描述

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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 17:20:53