最近在写一个 6盘 的第三方客户端,设计上是想要在文件管理页面底部加一个 BottomAppBar,随滚动隐藏。因为需要实现上传文件/创建文件夹/传输列表,如果都是作为图标放在 BottomAppBar 里,感觉不美观,所以准备浓缩在 FloatingActionButton 里。
但是查了才发现 Google 官方虽然出了 FloatingActionMenu 的设计规格,但是 Material 包里却至今没实现(都麻了,不少设计规格里有的功能一直都还没做)。
找了几个第三方库,star 数多的基本都不再更新了。就算不更了,本来有侥幸心理想拿来看看能不能用,测试完发现,如果单单作为 FAB 那完全没问题,但是就没办法跟 BottomAppBar 好好联动了。
官方的 FloatingActionButton 是可以在加了 app:layout_anchor="@id/bottomAppBar" 的情况下,自动的让 BottomAppBar 顶部出现一个嵌合 FAB 的凹槽的,如果用了第三方库那没办法出现……
思考了一下,应该可以通过预先写定 FAB 菜单所需的 FAB 后,将其设置为 invisible,然后在点击主 FAB 时通过配合动画让它们出现,最终实现效果如下:

在此之前
由于 BottomAppBar 和 FloatingActionButton 都是 Google Design 包里的控件,因此需要导包。
首先要在确保 Project 级的 build.gradle 中存在 google()
allprojects {
    repositories {
        google()
        jcenter()
        maven { url "https://jitpack.io" }
    }
}
接着在 Module 级 的 build.gradle 中加入:
dependencies {
  implementation 'com.google.android.material:material:<version>'
}
<version> 可以参考 Google's Maven Repository 或者 MVN Repository。
动画文件
这里用到 Clans/FloatingActionButton@GitHub 项目里的四种动画,分别是:
- 「按比例放大」「按比例缩小」(菜单 FAB)
 - 「从左到右进入」「从右到左进入」(主 FAB)
 
在 res 目录中新建 anim 文件夹,分别新建以下四个文件并填入对应内容。
fab_scale_up.xml
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:interpolator/overshoot"
    android:fromXScale="0.0"
    android:toXScale="1.0"
    android:fromYScale="0.0"
    android:toYScale="1.0"
    android:pivotX="50%"
    android:pivotY="50%"
    android:duration="200" />
fab_scale_down.xml
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:interpolator/accelerate_quint"
    android:fromXScale="1.0"
    android:toXScale="0.0"
    android:fromYScale="1.0"
    android:toYScale="0.0"
    android:pivotX="50%"
    android:pivotY="50%"
    android:duration="200" />
fab_slide_in_from_left.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:interpolator/overshoot">
    <alpha
        android:fromAlpha="0"
        android:toAlpha="1"
        android:duration="300" />
    <translate
        android:fromXDelta="-15%p"
        android:toXDelta="0"
        android:duration="200" />
</set>
fab_slide_in_from_right.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:interpolator/overshoot">
    <alpha
        android:fromAlpha="0"
        android:toAlpha="1"
        android:duration="300" />
    <translate
        android:fromXDelta="15%p"
        android:toXDelta="0"
        android:duration="200" />
</set>
布局文件
打开你要加入 BottomAppBar 的布局文件,向其中加入:
<?xml version="1.0" encoding="utf-8"?>
<......>
    <com.google.android.material.bottomappbar.BottomAppBar
        android:id="@+id/bottomAppBar"
        style="@style/Widget.MaterialComponents.BottomAppBar.Colored"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        app:hideOnScroll="true"
        app:menu="@menu/file_bottom_appbar"
        app:navigationContentDescription="@string/file_to_parent_folder"
        app:navigationIcon="@drawable/ic_baseline_keyboard_capslock_24" />
    <LinearLayout
        android:id="@+id/file_fab_menu_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:layout_marginBottom="100dp"
        android:gravity="center"
        android:orientation="vertical">
        <include layout="@layout/file_fab_upload_layout"/>
        <include layout="@layout/file_fab_create_folder_layout"/>
        <include layout="@layout/file_fab_transmission_layout"/>
    </LinearLayout>
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/file_fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:contentDescription="@string/file_upload"
        app:backgroundTint="@color/colorAccent"
        app:layout_anchor="@id/bottomAppBar"
        app:srcCompat="@drawable/ic_baseline_add_24"
        app:tint="@android:color/white" />
</......>
实际上就是一个 BottomAppBar 和一个 FloatingActionButton,另外再加了一个 LinearLayout 用来放菜单 FAB。控件涉及到的菜单、图标、内容描述、颜色等请根据实际情况自行修改。(注:为 BottomAppBar 添加 app:hideOnScroll="true" 属性可以在页面滑动时自动向下隐藏,但保留主 FAB)
由于我们调用了三个布局文件,这里也需要贴一下,因为三个基本上一样,只需要修改图标等就行,所以只贴一个。
file_fab_upload_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    android:id="@+id/file_fab_upload"
    android:visibility="invisible"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <TextView
        android:layout_marginTop="4dp"
        android:layout_marginBottom="12dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/file_upload"
        android:layout_marginEnd="10dp"
        android:textSize="16sp"
        android:textColor="@android:color/white"
        android:background="@drawable/fab_label_style"
        android:padding="5dp"
        android:elevation="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/file_fab_upload_button"
        app:layout_constraintTop_toTopOf="parent" />
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/file_fab_upload_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:layout_marginBottom="12dp"
        android:animateLayoutChanges="true"
        android:contentDescription="@string/file_upload"
        android:visibility="visible"
        app:backgroundTint="@color/colorAccent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_baseline_cloud_upload_24"
        app:tint="@android:color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
其中所用到的 @drawable/fab_label_style 如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#424242" />
    <corners android:radius="5dp" />
    <padding
        android:left="10dp"
        android:top="10dp"
        android:right="10dp"
        android:bottom="10dp" />
</shape>
Kotlin 代码
修改对应的 Activity 文件:
companion object {
    ...
    var isShowFabMenu = false
    lateinit var showAnimation: Animation
    lateinit var hideAnimation: Animation
    lateinit var showMenuAnimation: Animation
    lateinit var hideMenuAnimation: Animation
    ...
}
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.XXXXXXXXXX)
    initFAB()
    ...
}
private fun initFAB() {
    showAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_scale_up)
    hideAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_scale_down)
    showMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_slide_in_from_left)
    hideMenuAnimation = AnimationUtils.loadAnimation(this, R.anim.fab_slide_in_from_right)
    file_fab.setOnClickListener {
        if (!isShowFabMenu) {
            //还没显示菜单
            showMenu()
        } else {
            //显示菜单了
            hideMenu()
        }
    }
}
private fun showMenu() {
    file_fab.startAnimation(showMenuAnimation)
    file_fab.setImageDrawable(
        ContextCompat.getDrawable(
            this,
            R.drawable.ic_baseline_close_24
        )
    )
    file_fab_upload.startAnimation(showAnimation)
    file_fab_create_folder.startAnimation(showAnimation)
    file_fab_transmission.startAnimation(showAnimation)
    file_fab_upload.visibility = View.VISIBLE
    file_fab_create_folder.visibility = View.VISIBLE
    file_fab_transmission.visibility = View.VISIBLE
    isShowFabMenu = true
}
private fun hideMenu() {
    file_fab.startAnimation(hideMenuAnimation)
    file_fab.setImageDrawable(
        ContextCompat.getDrawable(
            this,
            R.drawable.ic_baseline_add_24
        )
    )
    file_fab_upload.startAnimation(hideAnimation)
    file_fab_create_folder.startAnimation(hideAnimation)
    file_fab_transmission.startAnimation(hideAnimation)
    file_fab_upload.visibility = View.INVISIBLE
    file_fab_create_folder.visibility = View.INVISIBLE
    file_fab_transmission.visibility = View.INVISIBLE
    isShowFabMenu = false
}
监听主 FAB 的点击事件,并预先用 isShowFabMenu 作为 FLAG 来判断当前 FAB 菜单是否展开。
- 未展开,则执行 
showMenu()方法,为主 FAB 设置「从左到右进入」动画,并且把图标改成 ×。同时为菜单 FAB 各设置「按比例放大」,并使其visibility变为VISIBLE。最后将 FLAG 设置为true,表明当前菜单已展开。 - 展开,则执行 
hideMenu()方法,为主 FAB 设置「从右到左进入」动画,并且把图标改回 +。同时为菜单 FAB 各设置「按比例缩小」,并使其visibility变为 INVISIBLE。最后将 FLAG 设置为false,表明当前菜单已不再展开。 
优货购 Chrome 87.0.4280.141
安卓还是原生流畅一些,之前用html5写的很卡啊
TigerBeanst Edge Chromium 89.0.760.0
原生控件肯定是要流畅一点……H5 还要受到 WebView 的制约