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

Android实现仿谷歌日历FAB:仅内部图标旋转而非整个按钮

你之前的方案问题出在把旋转动画绑定到了整个FAB控件上,才会导致方形按钮跟着旋转出现违和感。不需要叠放两个主FAB,单FAB就能实现目标效果,所有形变、旋转动画只作用在FAB内部的图标上即可。

实现步骤

1. 修改布局文件

删掉原来叠放在底部的第二个主FAB,只保留1个主FAB和上层的迷你功能FAB:

<?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:id="@+id/constraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- 迷你功能FAB -->
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab_task"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="24dp"
        android:visibility="invisible"
        app:backgroundTint="@color/fab_calendar"
        app:fabSize="mini"
        app:layout_constraintBottom_toTopOf="@+id/fab_main"
        app:layout_constraintEnd_toEndOf="@+id/fab_main"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="@+id/fab_main"
        app:srcCompat="@drawable/ic_baseline_task_alt_24"
        app:tint="@color/white" />

    <!-- 唯一主FAB,初始显示加号图标 -->
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab_main"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="16dp"
        android:onClick="mainFabClickListener"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:tint="@color/black"
        app:srcCompat="@drawable/ic_baseline_add_24" />

</androidx.constraintlayout.widget.ConstraintLayout>

2. 编写Java逻辑

核心规则:不对FAB本身加任何旋转、缩放动画,所有旋转、透明度过渡只作用在FAB内部的图标控件上,FAB本身仅做背景色、图标色调的平滑渐变。

private FloatingActionButton mFabMain;
private FloatingActionButton mFabTask;
private boolean isFabExpanded = false;
private int colorNormal;
private int colorCalendar;
private final long ANIM_DURATION = 300L;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mFabMain = findViewById(R.id.fab_main);
    mFabTask = findViewById(R.id.fab_task);
    colorNormal = ContextCompat.getColor(this, R.color.design_default_color_secondary);
    colorCalendar = ContextCompat.getColor(this, R.color.fab_calendar);
}

public void mainFabClickListener(View view) {
    // 拿到FAB内部承载图标的子View
    View iconView = mFabMain.getChildAt(0);
    if (!isFabExpanded) {
        // 加号转日历的动画
        ObjectAnimator rotateOut = ObjectAnimator.ofFloat(iconView, "rotation", 0f, 180f);
        ObjectAnimator alphaOut = ObjectAnimator.ofFloat(iconView, "alpha", 1f, 0f);
        AnimatorSet outSet = new AnimatorSet();
        outSet.playTogether(rotateOut, alphaOut);
        outSet.setDuration(ANIM_DURATION/2);
        outSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                // 旧图标完全淡出后切换新图标,执行反向转回来的入场动画
                mFabMain.setImageResource(R.drawable.ic_baseline_event_24);
                iconView.setRotation(-180f);
                ObjectAnimator rotateIn = ObjectAnimator.ofFloat(iconView, "rotation", -180f, 0f);
                ObjectAnimator alphaIn = ObjectAnimator.ofFloat(iconView, "alpha", 0f, 1f);
                // FAB背景、图标颜色同步渐变
                ObjectAnimator bgColor = ObjectAnimator.ofArgb(mFabMain, "backgroundTint", colorNormal, colorCalendar);
                ObjectAnimator iconTint = ObjectAnimator.ofArgb(mFabMain, "imageTint", Color.BLACK, Color.WHITE);
                AnimatorSet inSet = new AnimatorSet();
                inSet.playTogether(rotateIn, alphaIn, bgColor, iconTint);
                inSet.setDuration(ANIM_DURATION/2);
                inSet.start();
                toggleMiniFabsVisibility(true);
                isFabExpanded = true;
            }
        });
        outSet.start();
    } else {
        // 日历转回加号的反向动画
        ObjectAnimator rotateOut = ObjectAnimator.ofFloat(iconView, "rotation", 0f, -180f);
        ObjectAnimator alphaOut = ObjectAnimator.ofFloat(iconView, "alpha", 1f, 0f);
        AnimatorSet outSet = new AnimatorSet();
        outSet.playTogether(rotateOut, alphaOut);
        outSet.setDuration(ANIM_DURATION/2);
        outSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mFabMain.setImageResource(R.drawable.ic_baseline_add_24);
                iconView.setRotation(180f);
                ObjectAnimator rotateIn = ObjectAnimator.ofFloat(iconView, "rotation", 180f, 0f);
                ObjectAnimator alphaIn = ObjectAnimator.ofFloat(iconView, "alpha", 0f, 1f);
                ObjectAnimator bgColor = ObjectAnimator.ofArgb(mFabMain, "backgroundTint", colorCalendar, colorNormal);
                ObjectAnimator iconTint = ObjectAnimator.ofArgb(mFabMain, "imageTint", Color.WHITE, Color.BLACK);
                AnimatorSet inSet = new AnimatorSet();
                inSet.playTogether(rotateIn, alphaIn, bgColor, iconTint);
                inSet.setDuration(ANIM_DURATION/2);
                inSet.start();
                toggleMiniFabsVisibility(false);
                isFabExpanded = false;
            }
        });
        outSet.start();
    }
}

private void toggleMiniFabsVisibility(boolean show) {
    if (show) {
        mFabTask.setVisibility(View.VISIBLE);
        mFabTask.setTranslationY(20f);
        mFabTask.setAlpha(0f);
        mFabTask.animate().translationY(0f).alpha(1f).setDuration(200L).start();
    } else {
        mFabTask.animate().translationY(20f).alpha(0f).setDuration(200L)
                .withEndAction(() -> mFabTask.setVisibility(View.INVISIBLE))
                .start();
    }
}

如果需要和谷歌日历完全一致的丝滑图标变形效果,可以把硬切图标的逻辑替换为AnimatedVectorDrawable,实现加号到日历图标的路径形变动画即可。

内容的提问来源于stack exchange,提问作者backslashV

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 03:15:47