Android Design Support Library--FloatingActionButton及其Behavior的使用

weixin_34138377發表於2016-10-27

引言

如果說前面提到的TextInputLayout、SnackBar的應用還不是很常見的話,那麼今天提到的FloatingActionButton絕對是一個隨處可見的Material Design控制元件了,無論是我們常用的知乎、印象筆記或者是可愛的谷歌全家桶套裝都可以見到FloatingActionButton的身影,今天就來說說FloatingActionButton。

關於使用

其實我相信很多人都用過了Material Design控制元件了,但是還是要說一下,畢竟有些人接觸的晚一些,一些人接觸的早一些,先從最簡單的使用看起:

屬性值 作用
app:elevation 設定FAB未按下時的景深
app:pressedTranslationZ 設定FAB按下時的景深
app:fabSize 設定FAB的大小,預設只有normal和mini兩種選項
app:borderWidth 設定FAB的邊框寬度
android:src 設定FAB的drawaber
app:rippleColor 設定FAB按下時的背景色
app:backgroundTint 設定FAB未按下時的背景色
app:layout_anchor 設定FAB的錨點
app:layout_anchorGravity 設定FAB相對於錨點的位置
app:layout_behavior 設定FAB的Behavior行為屬性

大部分的屬性還是很好理解的,這裡要提一下幾個注意的點

  • app:borderWidth :這個一般設定為0dp,不然的話在4.1的sdk上FAB會顯示為正方形,而且在5.0以後的sdk沒有陰影效果
  • app:rippleColor:當我使用com.android.support:design:23.2.0 的時候這個屬性會失效,建議使用最新的 com.android.support:design:23.3.0' 或者適當的降低版本
  • android:layout_marginBottom :由於FAB 支援庫仍然存在一些 bug,在 Kitkat 和 Lollipop 中分別執行示例程式碼,可以看到如下結果:

Lollipop 中的 FAB:

735909-7f8ca975b3c3ea12.jpg
Lollipop 中的 FAB

Kitkat 中的 FAB:

735909-eeef434197918cee.jpg
Kitkat 中的 FAB

很容易看出,Lollipop 中存在邊緣顯示的問題。為了解決此問題,API21+ 的版本統一定義底部與右邊緣空白為 16dp,Lollipop 以下版本統一設定為 0dp.解決辦法:

values/dimens.xml

<dimen name="fab_margin_right">0dp</dimen>
<dimen name="fab_margin_bottom">0dp</dimen>

values-v21/dimens.xml

<dimen name="fab_margin_right">16dp</dimen>
<dimen name="fab_margin_bottom">16dp</dimen>

佈局檔案的 FAB 中,也設定相應的值:

<android.support.design.widget.FloatingActionButton
    ...
    ...
    android:layout_marginBottom="@dimen/fab_margin_bottom"
    android:layout_marginRight="@dimen/fab_margin_right"/>

以上這段話出處

  • app:layout_anchor:和app:layout_anchorGravity屬性一起搭配使用,可以做出不同的效果:

最簡單的使用

 <android.support.design.widget.FloatingActionButton
        ...
        ...
        app:layout_anchor="@id/mRecycleView"
        app:layout_anchorGravity="bottom|right|end"
        ...
         />
735909-59d7a7715b7b404f.jpg
最簡單的使用

更酷炫的效果

  <android.support.design.widget.FloatingActionButton  
        ...
        app:layout_anchor="@id/collapsingToolbarLayout"  
        app:layout_anchorGravity="bottom|center"  
        ...
         />  
735909-5b05a2607b053ab1.jpg
更酷炫的效果

這張圖片出處

可以看出我們只要使用app:layout_anchor屬性設定一個控制元件作為FAB的錨,然後通過app:layout_anchorGravity 屬性放置FAB在這個相對的錨的位置,就能做出你想要的效果。

  • app:layout_behavior:這個屬性接下來會重點講,也就是這個屬性成就了Material Design的眾多動畫互動效果,我們熟知的SnackBar配合FAB可以側滑以及APPBarLayout等動畫效果都是通過Behavior做出來的

自定義Behavior

如果你還記得這張圖的話:

735909-0412fef046d56bc2.jpg
互動效果

或者說你見過這種互動效果:

735909-b7811a2ad183a2d8.jpg
互動效果

其實這些都是通過Behavior這個類做出來的,以上的兩種動畫都是預設自帶的Behavior,在CoordinatorLayout 內部有對Behavior類的描述:

/**
     * Interaction behavior plugin for child views of {@link CoordinatorLayout}.
     *
     * <p>A Behavior implements one or more interactions that a user can take on a child view.
     * These interactions may include drags, swipes, flings, or any other gestures.</p>
     *
     * @param <V> The View type that this Behavior operates on
     */
    public static abstract class Behavior<V extends View> {

可以看到這是一個抽象類,我們可以在各個Material Design去實現這個類,這裡提到FAB,我們可以找一下FAB中的預設Behavior互動的實現:

 /**
     * Behavior designed for use with {@link FloatingActionButton} instances. It's main function
     * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do
     * not cover them.
     */
    public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
        // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is
        // because we can use view translation properties which greatly simplifies the code.
        private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11;

這裡只貼出一部分,如果英文不差的話看得懂註釋的意思:大致就是說我們這裡只提供API 11以上的Snackbar和FAB的運動互動效果,也就是我們上面動圖中看到的效果:當出現了一個SnackBar時候,FAB會自動向上移動一段距離,當SnackBar消失的時候FAB會回到原來位置,那麼如何定義一個屬於我們自己的Behavior,先來看看需要用到的知識:

其實細分的話有兩種情況:
1、當一個View的變化依賴於另一個View的尺寸、位置等變化的時候,我們只需要關注以下兩種方法:

 * @param parent 第一個引數不用解釋吧
 * @param 你要依賴別的View的那個View
 * @param dependency 你要依賴的View
 * @return return 如果找到了你依賴的那個View就返回true
         * @see #onDependentViewChanged(CoordinatorLayout, android.view.View, android.view.View)
         */
        public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }
* @param parent 同上,不解釋
* @param child 同上
* @param dependency 同上
* @return 如果這個Behavior改變了child的位置或者尺寸大小就返回true
         */
        public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }

其實FAB裡面就是實現了這兩種方法來與SnackBar互動的,看一下標準寫法:

 @Override
        public boolean layoutDependsOn(CoordinatorLayout parent,
                FloatingActionButton child, View dependency) {
            // We're dependent on all SnackbarLayouts (if enabled)
            return SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout;
        }
        ...
        ...
        @Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
                View dependency) {
            if (dependency instanceof Snackbar.SnackbarLayout) {
                updateFabTranslationForSnackbar(parent, child, dependency);
            } else if (dependency instanceof AppBarLayout) {
                // If we're depending on an AppBarLayout we will show/hide it automatically
                // if the FAB is anchored to the AppBarLayout
                updateFabVisibility(parent, (AppBarLayout) dependency, child);
            }
            return false;
        }

2、另一種情況是當一個View監聽CoordinatorLayout內部滑動的View進行互動時,我們需要關注的方法稍微多一點,這些方法都寫在了NestedScrollingParent介面裡面,而且CoordinatorLayout已經對這個介面有了預設實現:

onStartNestedScroll

         * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
         *                          associated with
         * @param child the child view of the CoordinatorLayout this Behavior is associated with
         * @param directTargetChild the child view of the CoordinatorLayout that either is or
         *                          contains the target of the nested scroll operation
         * @param target the descendant view of the CoordinatorLayout initiating the nested scroll
         * @param nestedScrollAxes the axes that this nested scroll applies to. See
         *                         {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
         *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL} 滑動時是橫軸和縱軸
         * @return true if the Behavior wishes to accept this nested scroll
         *
         * @see NestedScrollingParent#onStartNestedScroll(View, View, int)
         */
        public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                V child, View directTargetChild, View target, int nestedScrollAxes) {
            return false;
        }

onNestedPreScroll

         * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
         *                          associated with
         * @param child the child view of the CoordinatorLayout this Behavior is associated with
         * @param target the descendant view of the CoordinatorLayout performing the nested scroll
         * @param dx the raw horizontal number of pixels that the user attempted to scroll
         * @param dy the raw vertical number of pixels that the user attempted to scroll
         * @param consumed out parameter. consumed[0] should be set to the distance of dx that
         *                 was consumed, consumed[1] should be set to the distance of dy that
         *                 was consumed
         *
         * @see NestedScrollingParent#onNestedPreScroll(View, int, int, int[])
         */
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                int dx, int dy, int[] consumed) {
            // Do nothing
        }

onNestedFling

         * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
         *                          associated with
         * @param child the child view of the CoordinatorLayout this Behavior is associated with
         * @param target the descendant view of the CoordinatorLayout performing the nested scroll
         * @param velocityX horizontal velocity of the attempted fling
         * @param velocityY vertical velocity of the attempted fling
         * @param consumed true if the nested child view consumed the fling
         * @return true if the Behavior consumed the fling
         *
         * @see NestedScrollingParent#onNestedFling(View, float, float, boolean)
         */
        public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
                float velocityX, float velocityY, boolean consumed) {
            return false;
        }

onNestedScroll

         * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
         *                          associated with
         * @param child the child view of the CoordinatorLayout this Behavior is associated with
         * @param target the descendant view of the CoordinatorLayout performing the nested scroll
         * @param dxConsumed horizontal pixels consumed by the target's own scrolling operation
         * @param dyConsumed vertical pixels consumed by the target's own scrolling operation
         * @param dxUnconsumed horizontal pixels not consumed by the target's own scrolling
         *                     operation, but requested by the user
         * @param dyUnconsumed vertical pixels not consumed by the target's own scrolling operation,
         *                     but requested by the user
         *
         * @see NestedScrollingParent#onNestedScroll(View, int, int, int, int)
         */
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
            // Do nothing
        }

如果是碼農的話上面的英文註釋應該不難吧,這四個方法的區別如下:

  • onStartNestedScroll :當你想要初始化一個滑動的時候呼叫
  • onNestedPreScrollonNestedScroll:存在著兩個方法的原因是一些Behaviors(比如和AppBarLayout使用的)可能會消費掉部分滾動事件,我們可以在onNestedPreScroll方法內部計算需要滾動的距離,具體的話請看這裡
  • onNestedScroll:當target正嘗試滑動或者已經滑動時候呼叫這個方法
  • onNestedFling:看到Fling就明白是這是Fling情況下呼叫的方法,Fling最直觀的體現是你滑動一個ListView時鬆手的時候ListView還會因為慣性自動滑動一小段距離

這麼看可能太籠統了,看一下這一類Behavior的實際體現,我們自己自定義一個Behavior:

public class FadeBehavior extends FloatingActionButton.Behavior {


    /**
     * 因為是在XML中使用app:layout_behavior定義靜態的這種行為,
     * 必須實現一個建構函式使佈局的效果能夠正常工作。
     * 否則 Could not inflate Behavior subclass error messages.
     * @param context
     * @param attrs
     */
    public FadeBehavior(Context context, AttributeSet attrs) {
        super();
    }

    /**
     * 處理垂直方向上的滾動事件
     *
     *  @param coordinatorLayout
     *  @param child
     *  @param directTargetChild
     *  @param target
     *  @param nestedScrollAxes
     *  @return
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                                       FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) {

        // Ensure we react to vertical scrolling
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
                super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
                        nestedScrollAxes);
    }

    /**
     * 檢查Y的位置,並決定按鈕是否動畫進入或退出
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dxConsumed
     * @param dyConsumed
     * @param dxUnconsumed
     * @param dyUnconsumed
     */
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child,
                               View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed,
                dyUnconsumed);

        if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
            // User scrolled down and the FAB is currently visible -> hide the FAB
            child.hide();
        } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
            // User scrolled up and the FAB is currently not visible -> show the FAB
            child.show();
        }
    }
}

這裡繼承了FAB的Behavior寫了一個我們自己的實現,注意實現自己的Behavior的時候一定要重寫兩個引數的構造方法,因為CoordinatorLayout會從我們在XML中定義的app:layout_behavior屬性去找這個Behavior,瞭解自定義View的對這個應該不會陌生,一般的寫法是:

app:layout_behavior=".FadeBehavior "

在查資料的過程中發現很多人把自定義Behavior類所在的包名也寫進去了,其實親測沒必要這樣做,而且CoordinatorLayout裡面也有專門的方法去解析:

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }

        final String fullName;
        if (name.startsWith(".")) {
            // Relative to the app package. Prepend the app package name.
            fullName = context.getPackageName() + name;
        } else if (name.indexOf('.') >= 0) {
            // Fully qualified package name.
            fullName = name;
        } else {
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                    ? (WIDGET_PACKAGE_NAME + '.' + name)
                    : name;
        }

        try {
            Map<String, Constructor<Behavior>> constructors = sConstructors.get();
            if (constructors == null) {
                constructors = new HashMap<>();
                sConstructors.set(constructors);
            }
            Constructor<Behavior> c = constructors.get(fullName);
            if (c == null) {
                final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                        context.getClassLoader());
                c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                c.setAccessible(true);
                constructors.put(fullName, c);
            }
            return c.newInstance(context, attrs);
        } catch (Exception e) {
            throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
        }
    }

可以看到用這種方式的系統會自動給我們加上包名,寫太多反而顯的累贅,這個自定義Behavior應該很好理解,效果就是隨著RecycleView的滑動FAB會隱藏/顯示,是一個很常見的效果:

735909-8e9af07adf1d369e.jpg
常見效果

只要向上滾動FAB就會消失,向下滾動FAB就是顯示,這裡要注意的是FAB可以與RecycleView形成這種效果,但是暫時並不支援ListView,沒關係,反正RecycleView當成ListView來用就好,接下來仿照實現知乎的FAB效果的實現,先看一下知乎的效果:

735909-08e8d14c491535e9.jpg
知乎的效果

可以很清楚的看到FAB隨著RecycleView的滑動呈現出滾動推出的效果,並且點選FAB會出現旋轉效果並且彈出一個蒙版,我們可以先自定義一個用於執行FAB旋轉的Behavior,可以看到這裡FAB是逆時針旋轉135度,那麼程式碼就可以這麼寫:

public class RotateBehavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
    private static final String TAG = RotateBehavior.class.getSimpleName();

    public RotateBehavior() {

    }

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

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, FloatingActionButton child, View dependency) {
        return dependency instanceof Snackbar.SnackbarLayout;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child, View dependency) {
        float translationY = getFabTranslationYForSnackBar(parent, child);
        float percentComplete = -translationY / dependency.getHeight();
        child.setRotation(-135 * percentComplete);
        child.setTranslationY(translationY);
        return true;
    }

    private float getFabTranslationYForSnackBar(CoordinatorLayout parent,
                                                FloatingActionButton fab) {
        float minOffset = 0;
        final List<View> dependencies = parent.getDependencies(fab);
        for (int i = 0, z = dependencies.size(); i < z; i++) {
            final View view = dependencies.get(i);
            if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(fab, view)) {
                //view.getHeight()固定為144
                //ViewCompat.getTranslationY(view)從144-0,再從0-144
                minOffset = Math.min(minOffset,
                        ViewCompat.getTranslationY(view) - view.getHeight());
                Log.d("TranslationY",ViewCompat.getTranslationY(view)+"");
                Log.d("Height",view.getHeight()+"");
            }
        }

        return minOffset;
    }
}

這裡可能就這段程式碼比較難理解:

minOffset = Math.min(minOffset,
                        ViewCompat.getTranslationY(view) - view.getHeight());

我在上面打了兩個Log,分別得出了ViewCompat.getTranslationY(view)view.getHeight() ,這樣看程式碼就比較容易看懂,但是為什麼ViewCompat.getTranslationY(view) 是正數呢,這裡的的View我們都知道指的是SnackBar,我們都知道向上移動的話getTranslationY 應該是負數啊,其實SnackBar的原始碼中有一個這樣的動作:

ViewCompat.setTranslationY(mView, mView.getHeight());
            ViewCompat.animate(mView)
                    .translationY(0f)
                    .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
                    .setDuration(ANIMATION_DURATION)

也就是說SnackBar一開始就向下移動了mView.getHeight()的長度,當SnackBar出現的時候只是向著它原來的位置移動,本質上還是相當於從它原來的位置移動了一段距離,只是這個距離隨著SnackBar向上浮動的越來越多而變得越來越小,直至回到原來的位置,這麼說應該可以理解了,接下來我們在XML檔案中加入一個TextView作為蒙版:

    <TextView
        android:id="@+id/hide"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffff"
        android:visibility="gone" />

因為CoordinatorLayout相當於幀佈局是一層一層疊加的所以這個蒙版放在RecycleView和FAB中間,整個佈局程式碼:

<?xml version="1.0" encoding="utf-8"?>
<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/coor"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/mRecycleView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </android.support.v7.widget.RecyclerView>

    <TextView
        android:id="@+id/hide"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffff"
        android:visibility="gone" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="@dimen/fab_margin_bottom"
        android:layout_marginEnd="@dimen/fab_margin_right"
        android:src="@mipmap/plus"
        app:backgroundTint="#0767C8"
        app:borderWidth="0dp"
        app:elevation="6dp"
        app:fabSize="normal"
        app:layout_anchor="@id/mRecycleView"
        app:layout_anchorGravity="bottom|right|end"
        app:layout_behavior=".FadeBehavior"
        app:pressedTranslationZ="12dp"
        app:rippleColor="#0767C8" />

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

看看效果:

735909-85f0b2d01a407941.jpg
效果

是不是有一個很奇怪的地方,知乎的FAB並沒有SnackBar彈出啊,那就說明一開始的思路錯了,但是一個FAB只能設定一個app:layout_behavior ,如果我們把這個Behavior用作FAB的旋轉效果那麼FAB的滾動移出檢視的效果就沒了,所以換一種思路,用Object動畫來做FAB的旋轉效果:

  //開始旋轉
    public void turnLeft(View v) {
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(v, "rotation", 0, -155, -135);
        objectAnimator.setDuration(300);
        objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        objectAnimator.start();
        hide.setVisibility(View.VISIBLE);
        AlphaAnimation alphaAnimation = new AlphaAnimation(0, 0.75f);
        alphaAnimation.setDuration(300);
        alphaAnimation.setFillAfter(true);
        hide.startAnimation(alphaAnimation);
        hide.setClickable(true);
        isOpen = true;
    }

    //回到起始位置
    public void turnRight(View v) {
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(v, "rotation", -135, 20, 0);
        objectAnimator.setDuration(300);
        objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        objectAnimator.start();
        hide.setVisibility(View.GONE);
        AlphaAnimation alphaAnimation = new AlphaAnimation(0.75f, 0);
        alphaAnimation.setDuration(300);
        alphaAnimation.setFillAfter(true);
        hide.startAnimation(alphaAnimation);
        hide.setClickable(false);
        isOpen = false;
    }
//注:hide就是TextView控制元件(蒙版)

然後實現FAB的滾動移出檢視效果的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) {
        //垂直滾動
        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) {
            //withLayer()使動畫中的某些操作變得更順暢,加速渲染,API 14以後
            ViewCompat.animate(button).translationY(button.getHeight() + getMarginBottom(button)).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 {

        }
    }

    // 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).translationY(0)
                    .setInterpolator(INTERPOLATOR).withLayer().setListener(null)
                    .start();
        } else {

        }
    }

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

最後實現的效果:

735909-dfaae781fc36cd63.jpg
最終效果

這裡部分參考了仿知乎FloatingActionButton浮動按鈕動畫效果實現

至於FAB彈出的InBox這裡就不去實現了,比較麻煩,可以參考第三方的實現:

735909-22e7010e0101db39.jpg
第三方實現

FloatingActionButtonPlus

寫在末尾

主要參考:
浮動操作按鈕的選擇
FloatingActionButton.Behavior
codepath教程:浮動操作按鈕詳解
Design Support Library (II): Floating Action Button
CoordinatorLayout高階用法-自定義Behavior

專案原始碼
GitHub地址

寫文章不容易,如果可以的話請給個贊

相關文章