如何像Google+应用程序中的FloatingActionButton一样制作Android动画?

17
我将FloatingActionButton设置到屏幕底部,并希望对该按钮进行动画处理。
  • 向下滚动时隐藏
  • 向上滚动时显示
就像谷歌在他们的Google+应用中实现的那样。
我认为需要使用CoordinatorLayout和AppBarLayout,但如何实现与FloatingActionButton一起使用呢?

你也可以查看Google IO日程应用程序的BaseActivity,并相应地动画化fab。 - Raghunandan
4个回答

32

您可以使用默认的FloatingActionButton并使用app:layout_behavior属性更改其默认Behavior,以实现此目的:

您可以使用以下布局:

 <android.support.design.widget.CoordinatorLayout     
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/main_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
            app:layout_scrollFlags="scroll|enterAlways" />

    </android.support.design.widget.AppBarLayout>

    // Your layout, for example a RecyclerView
    <RecyclerView
         .....
         app:layout_behavior="@string/appbar_scrolling_view_behavior" />

    <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="end|bottom"
            android:layout_margin="@dimen/fab_margin"
            android:src="@drawable/ic_done"      
            app:layout_behavior="com.support.android.designlibdemo.ScrollAwareFABBehavior" />

    </android.support.design.widget.CoordinatorLayout>

使用app:layout_behavior,您可以定义自己的Behavior。使用onStartNestedScroll()onNestedScroll()方法,您可以与滚动事件交互。

您可以像这样使用一个Behavior。 您可以在此处找到原始代码。

public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
    private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
    private boolean mIsAnimatingOut = false;

    public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
        super();
    }

    @Override
    public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,
                                       final View directTargetChild, final View target, final int nestedScrollAxes) {
        // Ensure we react to vertical scrolling
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
                || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    @Override
    public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,
                               final View target, final int dxConsumed, final int dyConsumed,
                               final int dxUnconsumed, final int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
        if (dyConsumed > 0 && !this.mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
            // User scrolled down and the FAB is currently visible -> hide the FAB
            animateOut(child);
        } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
            // User scrolled up and the FAB is currently not visible -> show the FAB
            animateIn(child);
        }
    }

    // Same animation that FloatingActionButton.Behavior uses to hide the FAB when the AppBarLayout exits
    private void animateOut(final FloatingActionButton button) {
        if (Build.VERSION.SDK_INT >= 14) {
            ViewCompat.animate(button).scaleX(0.0F).scaleY(0.0F).alpha(0.0F).setInterpolator(INTERPOLATOR).withLayer()
                    .setListener(new ViewPropertyAnimatorListener() {
                        public void onAnimationStart(View view) {
                            ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
                        }

                        public void onAnimationCancel(View view) {
                            ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
                        }

                        public void onAnimationEnd(View view) {
                            ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
                            view.setVisibility(View.GONE);
                        }
                    }).start();
        } else {
            Animation anim = AnimationUtils.loadAnimation(button.getContext(), R.anim.fab_out);
            anim.setInterpolator(INTERPOLATOR);
            anim.setDuration(200L);
            anim.setAnimationListener(new Animation.AnimationListener() {
                public void onAnimationStart(Animation animation) {
                    ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
                }

                public void onAnimationEnd(Animation animation) {
                    ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
                    button.setVisibility(View.GONE);
                }

                @Override
                public void onAnimationRepeat(final Animation animation) {
                }
            });
            button.startAnimation(anim);
        }
    }

    // Same animation that FloatingActionButton.Behavior uses to show the FAB when the AppBarLayout enters
    private void animateIn(FloatingActionButton button) {
        button.setVisibility(View.VISIBLE);
        if (Build.VERSION.SDK_INT >= 14) {
            ViewCompat.animate(button).scaleX(1.0F).scaleY(1.0F).alpha(1.0F)
                    .setInterpolator(INTERPOLATOR).withLayer().setListener(null)
                    .start();
        } else {
            Animation anim = AnimationUtils.loadAnimation(button.getContext(), R.anim.fab_in);
            anim.setDuration(200L);
            anim.setInterpolator(INTERPOLATOR);
            button.startAnimation(anim);
        }
    }
}

1
为什么RecyclerViewapp:layout_behavior属性有一个字符串资源,而FloatingActionButton有一个字面上硬编码的字符串?它们是否都应该指向同一个Behavior类? - B W
5
您好,我可以帮助您进行翻译。以下是翻译的结果:您是否遇到了找不到R.anim.fab_inR.anim.fab_out的问题?我认为这些文件可能还没有上传到代码库里面。请问需要怎样帮忙呢? - Shajeel Afzal
对于那些像我一样仍然找不到R.anim.fab_in和fab_out的人,提供的链接中有一个新的提交,它更短且运行良好。我目前正在使用它,没有任何问题 :) - Bruce
2
fab_in.xml和fab_out.xml可以在https://github.com/abhikmitra/xyzreader/blob/master/XYZReader/build/intermediates/res/debug/anim找到。 - Ashish Dwivedi
希望这也能帮到你 ScrollAwareFABBehavior - Pratik Butani
显示剩余3条评论

5
截至本帖发布时,设计支持库中没有自动处理隐藏和显示FloatingActionButton的方法。我知道这一点,因为这是我在工作中的第一个任务。
你想到的方法是用来在创建Snackbar时上下动画FloatingActionButton的,如果使用CoordinatorLayout,这将有效。
以下是我的代码,参考了这个仓库。它有RecyclerViewAbsListView的监听器,可以自动处理按钮动画。你可以这样做:
button.show();

或者

button.hide();

要手动隐藏按钮,或者您可以调用:
button.attachToListView(listView);

并且

button.attachToRecyclerView(recyclerView);

这段代码的作用是在向下滚动时隐藏按钮,在向上滚动时显示按钮,无需进一步编码。

希望这能帮到你!

AnimatedFloatingActionButton:

public class AnimatedFloatingActionButton extends FloatingActionButton
{
    private static final int TRANSLATE_DURATION_MILLIS = 200;
    private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
    private boolean mVisible;

public AnimatedFloatingActionButton(Context context, AttributeSet attrs)
{
    super(context, attrs);
    Log.i("Abscroll", "mVisible" + mVisible);
}

public void show() {
    show(true);
}

public void hide() {
    hide(true);
}

public void show(boolean animate) {
    toggle(true, animate, false);
}

public void hide(boolean animate) {
    toggle(false, animate, false);
}

private void toggle(final boolean visible, final boolean animate, boolean force) {
    if (mVisible != visible || force) {
        mVisible = visible;
        int height = getHeight();
        if (height == 0 && !force) {
            ViewTreeObserver vto = getViewTreeObserver();
            if (vto.isAlive()) {
                vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        ViewTreeObserver currentVto = getViewTreeObserver();
                        if (currentVto.isAlive()) {
                            currentVto.removeOnPreDrawListener(this);
                        }
                        toggle(visible, animate, true);
                        return true;
                    }
                });
                return;
            }
        }
        int translationY = visible ? 0 : height + getMarginBottom();
        Log.i("Abscroll", "transY" + translationY);
        if (animate) {
            this.animate().setInterpolator(mInterpolator)
                    .setDuration(TRANSLATE_DURATION_MILLIS)
                    .translationY(translationY);
        } else {
            setTranslationY(translationY);
        }
    }
}

private int getMarginBottom() {
    int marginBottom = 0;
    final ViewGroup.LayoutParams layoutParams = getLayoutParams();
    if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
        marginBottom = ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin;
    }
    return marginBottom;
}

public void attachToListView(@NonNull AbsListView listView)
{
    listView.setOnScrollListener(new AbsListViewScrollDetector() {
        @Override
        void onScrollUp() {
            hide();
        }

        @Override
        void onScrollDown() {
            show();
        }

        @Override
        void setScrollThreshold() {
            setScrollThreshold(getResources().getDimensionPixelOffset(R.dimen.fab_scroll_threshold));
        }
    });
}

public void attachToRecyclerView(@NonNull RecyclerView recyclerView) {
    recyclerView.addOnScrollListener(new RecyclerViewScrollDetector() {
        @Override
        void onScrollUp() {
            hide();
        }

        @Override
        void onScrollDown() {
            show();
        }

        @Override
        void setScrollThreshold() {
            setScrollThreshold(getResources().getDimensionPixelOffset(R.dimen.fab_scroll_threshold));
        }
    });
}
}

AbsListViewScrollDetector:

abstract class AbsListViewScrollDetector implements AbsListView.OnScrollListener {
private int mLastScrollY;
private int mPreviousFirstVisibleItem;
private AbsListView mListView;
private int mScrollThreshold;

abstract void onScrollUp();

abstract void onScrollDown();

abstract void setScrollThreshold();

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
    if(totalItemCount == 0) return;
    if (isSameRow(firstVisibleItem)) {
        int newScrollY = getTopItemScrollY();
        boolean isSignificantDelta = Math.abs(mLastScrollY - newScrollY) > mScrollThreshold;
        Log.i("Abscroll", "mLastScrollY " + mLastScrollY);
        Log.i("Abscroll", "newScrollY " + newScrollY);
        if (isSignificantDelta) {
            Log.i("Abscroll", "sig delta");
            if (mLastScrollY > newScrollY) {
                onScrollUp();
                Log.i("Abscroll", "sig delta up");
            } else {
                onScrollDown();
                Log.i("Abscroll", "sig delta down");
            }
        }
        mLastScrollY = newScrollY;
    } else {
        if (firstVisibleItem > mPreviousFirstVisibleItem) {
            onScrollUp();
            Log.i("Abscroll", "prev up");
        } else {
            onScrollDown();
            Log.i("Abscroll", "prev down");
        }

        mLastScrollY = getTopItemScrollY();
        mPreviousFirstVisibleItem = firstVisibleItem;
    }
}

public void setScrollThreshold(int scrollThreshold) {
    mScrollThreshold = scrollThreshold;
    Log.i("Abscroll", "LView thresh " + scrollThreshold);
}

public void setListView(@NonNull AbsListView listView) {
    mListView = listView;
}

private boolean isSameRow(int firstVisibleItem) {
    return firstVisibleItem == mPreviousFirstVisibleItem;
}

private int getTopItemScrollY() {
    if (mListView == null || mListView.getChildAt(0) == null) return 0;
    View topChild = mListView.getChildAt(0);
    return topChild.getTop();
}
}

RecyclerViewScrollDetector:

abstract class RecyclerViewScrollDetector extends RecyclerView.OnScrollListener {
private int mScrollThreshold;

abstract void onScrollUp();

abstract void onScrollDown();

abstract void setScrollThreshold();

@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    boolean isSignificantDelta = Math.abs(dy) > mScrollThreshold;
    if (isSignificantDelta) {
        if (dy > 0) {
            onScrollUp();
            Log.i("Abscroll", "Rview up");
        } else {
            onScrollDown();
            Log.i("Abscroll", "RView down");
        }
    }
}

public void setScrollThreshold(int scrollThreshold) {
    mScrollThreshold = scrollThreshold;
    Log.i("Abscroll", "RView thresh " + scrollThreshold);
}
}

@kevCron,感谢您的回答,但我使用ScrollView,并编写了一个监听器来检测向上和向下滚动,就像您的ScollListener类一样,但隐藏和显示按钮不够流畅,而Google+应用程序具有漂亮的动画效果和平滑性。 - Mehrdad Faraji
@8m47x 不,它没有,FloatingActionButton 中没有任何处理滚动隐藏的方法。如果你阅读文档,Google 还没有实现它。 - Kevin Cronly
@MehrdadFaraji,如果你的不够流畅,我建议你使用我的,我正在观察它的工作,它非常流畅。或者你可以发布你的代码,我们可以审查一下,找出为什么不够流畅的原因。 - Kevin Cronly
1
fab_scroll_threshold的值是多少? - Shajeel Afzal
1
<dimen name="fab_scroll_threshold">4dp</dimen> @ShajeelAfzal - Atul O Holic

0

Google的设计支持库可以实现这一点。

尝试将此代码实现到您的布局文件中:

<android.support.design.widget.FloatingActionButton
      android:id="@+id/fab"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:src="@drawable/ic_add_black"
      android:layout_gravity="bottom|end"
      app:elevation="6dp"
      app:pressedTranslationZ="12dp"/>

在你的build.gradle中添加compile 'com.android.support:design:22.2.0'非常重要。如果你使用的是eclipse,还有另一种方法可以将这个库添加到你的项目中。

重要资源:
设计支持库(II):浮动操作按钮
Android设计支持库
Google发布了一个FABulous新的设计支持库[更新]


虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供链接作为参考。仅有链接的答案如果链接页面更改可能会变得无效。 - gnat
1
是的,抱歉。我真的不得不这样做。我是stackoverflow的新手 ;) - Tomblarom
2
@8m47x,感谢您的回答。但是我不是在问如何创建FloatingActionButton,我已经创建并使用了它。我的问题是如何像Google+应用程序一样为FloatingActionButton添加动画效果? - Mehrdad Faraji

0

ExtendedFloatingActionButton 足以实现您的目标,无需处理任何逻辑代码,只需在滚动时调用 extend()shrink()

完整示例:

enter image description here

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>


<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
    android:id="@+id/extendedFloatingButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="end|bottom"
    android:text="Reorder List"
    android:layout_margin="12dp"
    app:icon="@drawable/ic_reorder" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

在你的代码中:

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: 
       Int) {
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                extendedFloatingButton.extend()
            }
            super.onScrollStateChanged(recyclerView, newState)
        }

        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            if (dy > 0 || dy < 0 && extendedFloatingButton.isExtended) {
                extendedFloatingButton.shrink()
            }
        }
    })

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接