最近在写一个 6盘 的第三方客户端,设计上是想要在文件管理页面底部加一个 BottomAppBar,随滚动隐藏。因为需要实现上传文件/创建文件夹/传输列表,如果都是作为图标放在 BottomAppBar 里,感觉不美观,所以准备浓缩在 FloatingActionButton 里。

    但是查了才发现 Google 官方虽然出了 FloatingActionMenu 的设计规格,但是 Material 包里却至今没实现(都麻了,不少设计规格里有的功能一直都还没做)。

    找了几个第三方库,star 数多的基本都不再更新了。就算不更了,本来有侥幸心理想拿来看看能不能用,测试完发现,如果单单作为 FAB 那完全没问题,但是就没办法跟 BottomAppBar 好好联动了。

    官方的 FloatingActionButton 是可以在加了 app:layout_anchor="@id/bottomAppBar" 的情况下,自动的让 BottomAppBar 顶部出现一个嵌合 FAB 的凹槽的,如果用了第三方库那没办法出现……

    思考了一下,应该可以通过预先写定 FAB 菜单所需的 FAB 后,将其设置为 invisible,然后在点击主 FAB 时通过配合动画让它们出现,最终实现效果如下:

    在此之前

    由于 BottomAppBarFloatingActionButton 都是 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,表明当前菜单已不再展开。