1. 程式人生 > >FloatingActionButton屬性、用法,以及解析並解決sdk25以上只隱藏不顯示的問題

FloatingActionButton屬性、用法,以及解析並解決sdk25以上只隱藏不顯示的問題

懸浮按鈕(FloatingActionButton),在下文簡稱fab,今天我們來講講它的一些屬性與用法,以及解析並解決sdk25以上FloatingActionButton只隱藏不顯示的問題。

先展示一下動態圖
這裡寫圖片描述

本次開發環境基於sdk25.

使用之前要先引入design包

compile 'com.android.support:design:25.3.1'

xml屬性

<android.support.design.widget.FloatingActionButton
        android:id="@+id/contact_fab"
        android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_gravity="bottom|right" android:layout_margin="10dp" android:src="@mipmap/ic_launcher" app:backgroundTint="@color/gray" app:backgroundTintMode="multiply" app:borderWidth="0dp" app:elevation="@dimen
/activity_horizontal_margin"
app:fabSize="auto" app:pressedTranslationZ="@dimen/activity_horizontal_margin" app:rippleColor="@color/gray" app:useCompatPadding="true" />
  • app:backgroundTint 按鈕的背景顏色,不設定,預設使用theme中colorAccent的顏色
  • app:backgroundTintMode 按鈕背景顏色的模式,在設定screen的時候就跟其他模式有點區別,區別在顏色變了,其他不變,具體不詳,可忽略
  • app:borderWidth 該屬性如果不設定0dp,那麼在4.1的sdk上FAB會顯示為正方形,而且在5.0以後的sdk沒有陰影效果。所以設定為borderWidth=”0dp”
  • app:elevation 預設狀態下陰影大小。
  • app:fabSize 設定大小,該屬性有兩個值,分別為normal和mini,對應的大小分別為56dp和40dp
  • app:pressedTranslationZ 按鈕按下去的狀態下的陰影大小
  • app:rippleColor 設定點選時的背景顏色
  • app:useCompatPadding 是否使用相容的填充大小

用法

可與FloatingActionMenu或者CoordinatorLayout一起使用。在這裡只拿CoordinatorLayout來做示例。

利用recyclerView的上下滑動來使fab顯示或隱藏,點選fab顯示snackbar

佈局

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/contact_recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

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

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/contact_fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:layout_margin="10dp"
        android:src="@mipmap/ic_launcher"
        app:backgroundTint="@color/turquoise"
        app:backgroundTintMode="src_in"
        app:borderWidth="0dp"
        app:elevation="5dp"
        app:fabSize="auto"
        app:pressedTranslationZ="50dp"
        app:rippleColor="@color/gray"
        app:useCompatPadding="true"
        app:layout_anchor="@+id/contact_recyclerview"
        app:layout_anchorGravity="bottom|right|end"
        app:layout_behavior="com.voctex.ui.tablayout.other.ScrollingViewBehavior" />


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

程式碼實現

RecyclerView recyclerView= ((RecyclerView) findViewById(R.id.contact_recyclerview));

        recyclerView.setHasFixedSize(true);
        recyclerView.setItemAnimator(new DefaultItemAnimator());

        //設定一個垂直方向的layout manager
        int orientation = LinearLayoutManager.VERTICAL;
        recyclerView.setLayoutManager(new LinearLayoutManager(mContext, orientation, false));

        List<String> mList=new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            mList.add("位置為:"+i);
        }

        TabLayoutAdapter tabLayoutAdapter=new TabLayoutAdapter(recyclerView,mList);

        recyclerView.setAdapter(tabLayoutAdapter);


        FloatingActionButton fab= ((FloatingActionButton) findViewById(R.id.contact_fab));
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Snackbar.make(v,"floatingActionBtn",Snackbar.LENGTH_SHORT)
                        .setAction("action", new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                VtToast.s(mContext,"仙劍奇俠傳");
                            }
                        }).show();
            }
        });

在以上程式碼中,只需要在xml中為fab指定一個屬性就可以實現recyclerView在上下滑動時fab的顯示或隱藏了。

app:layout_behavior="com.voctex.ui.tablayout.other.ScrollingViewBehavior"

而值就是自定義的一個類,繼承於FloatingActionButton.Behavior,重寫onStartNestedScroll和onNestedScroll這兩個方法,相關程式碼如下:

package com.voctex.ui.tablayout.other;

import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by mac_xihao on 17/7/3.
 * (~ ̄▽ ̄)~ 嘛哩嘛哩哄
 */
public class ScrollingViewBehavior extends FloatingActionButton.Behavior {

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

    /**
     * 處理垂直方向上的滾動事件
     *
     * @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 > 10 && child.getVisibility() == View.VISIBLE) {
            // User scrolled down and the FAB is currently visible -> hide the FAB
            //執行隱藏的動畫
            child.hide();
        } else if (dyConsumed < -10 && child.getVisibility() != View.VISIBLE) {
            // User scrolled up and the FAB is currently not visible -> show the FAB
            //執行顯示的動畫
            child.show();
        }
    }
}

其實,到這裡是應該結束的了,實現起來是很簡單的,但是我在測試的時候卻發現了一個比較坑的問題,就是RecylerView在滑動的時候,只能隱藏,卻不顯示fab。

解析並解決sdk25 FloatingActionButton只隱藏不顯示的問題

這個問題坑呀,上網查看了不少人都是這麼實現的,都是可以隱藏顯示的,我就針對這個問題百度一下,發現有人在網上說sdk25以上的會出現不顯示fab的問題,具體問題出現在了一下程式碼中:

sdk25以上,CoordinatorLayout的onNestedScroll方法會多出一段程式碼

 @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed) {
        final int childCount = getChildCount();
        boolean accepted = false;

        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            //sdk25以上會多出這個判斷
            if (view.getVisibility() == GONE) {
                // If the child is GONE, skip...
                continue;
            }

            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);
                accepted = true;
            }
        }

        if (accepted) {
            onChildViewsChanged(EVENT_NESTED_SCROLL);
        }
    }

也就是CoordinatorLayout在滑動的時候,判斷子view是否被設定為GONE,如果是,直接執行下一次迴圈,然後就不回撥onNestedScroll,而我們自定義的那個類的onNestedScroll方法就不走了。

而在fab執行hide方法的時候,預設是把fab設定為GONE的,我們來看看fab中的hide方法

/**
     * Hides the button.
     * <p>This method will animate the button hide if the view has already been laid out.</p>
     */
    public void hide() {
        hide(null);
    }

    /**
     * Hides the button.
     * <p>This method will animate the button hide if the view has already been laid out.</p>
     *
     * @param listener the listener to notify when this view is hidden
     */
    public void hide(@Nullable OnVisibilityChangedListener listener) {
        hide(listener, true);
    }

    void hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser) {
        getImpl().hide(wrapOnVisibilityChangedListener(listener), fromUser);
    }

最後是呼叫了以下程式碼,並且fromUser預設為true,這個值很關鍵

getImpl().hide(wrapOnVisibilityChangedListener(listener), fromUser);

接下來我們繼續走下去,getImpl()是獲得哪個物件

private FloatingActionButtonImpl getImpl() {
        if (mImpl == null) {
            mImpl = createImpl();
        }
        return mImpl;
    }

    private FloatingActionButtonImpl createImpl() {
        final int sdk = Build.VERSION.SDK_INT;
        if (sdk >= 21) {
            return new FloatingActionButtonLollipop(this, new ShadowDelegateImpl(),
                    ViewUtils.DEFAULT_ANIMATOR_CREATOR);
        } else if (sdk >= 14) {
            return new FloatingActionButtonIcs(this, new ShadowDelegateImpl(),
                    ViewUtils.DEFAULT_ANIMATOR_CREATOR);
        } else {
            return new FloatingActionButtonGingerbread(this, new ShadowDelegateImpl(),
                    ViewUtils.DEFAULT_ANIMATOR_CREATOR);
        }
    }

在這裡我們預設用4.0以上的手機進行測試,所以獲得的物件是FloatingActionButtonIcs的例項,然後就進入看看它的hide方法是如何實現的。

@Override
    void hide(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) {
        if (isOrWillBeHidden()) {
            // We either are or will soon be hidden, skip the call
            return;
        }

        mView.animate().cancel();

        if (shouldAnimateVisibilityChange()) {
            mAnimState = ANIM_STATE_HIDING;

            mView.animate()
                    .scaleX(0f)
                    .scaleY(0f)
                    .alpha(0f)
                    .setDuration(SHOW_HIDE_ANIM_DURATION)
                    .setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR)
                    .setListener(new AnimatorListenerAdapter() {
                        private boolean mCancelled;

                        @Override
                        public void onAnimationStart(Animator animation) {
                            mView.internalSetVisibility(View.VISIBLE, fromUser);
                            mCancelled = false;
                        }

                        @Override
                        public void onAnimationCancel(Animator animation) {
                            mCancelled = true;
                        }

                        @Override
                        public void onAnimationEnd(Animator animation) {
                            mAnimState = ANIM_STATE_NONE;

                            if (!mCancelled) {
                                mView.internalSetVisibility(fromUser ? View.GONE : View.INVISIBLE,
                                        fromUser);
                                if (listener != null) {
                                    listener.onHidden();
                                }
                            }
                        }
                    });
        } else {
            // If the view isn't laid out, or we're in the editor, don't run the animation
            mView.internalSetVisibility(fromUser ? View.GONE : View.INVISIBLE, fromUser);
            if (listener != null) {
                listener.onHidden();
            }
        }
    }

直接看onAnimationEnd方法裡面,預設fromUser為true,所以在這裡fab是直接被設定為了Gone,而CoordinatorLayout的onNestedScroll方法裡的迴圈又判斷子view為Gone的時候直接跳出執行下次迴圈,這裡是很矛盾的。

那麼在什麼情況下fromUser為false呢?

因為hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser)方法訪問許可權為default,只有同個包裡面的類才能呼叫,我直接搜尋了fab這個方法的呼叫,發現了還有兩個方法呼叫了這個方法,分別是

private boolean updateFabVisibilityForAppBarLayout(CoordinatorLayout parent,
                AppBarLayout appBarLayout, FloatingActionButton child) {
            if (!shouldUpdateVisibility(appBarLayout, child)) {
                return false;
            }

            if (mTmpRect == null) {
                mTmpRect = new Rect();
            }

            // First, let's get the visible rect of the dependency
            final Rect rect = mTmpRect;
            ViewGroupUtils.getDescendantRect(parent, appBarLayout, rect);

            if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) {
                // If the anchor's bottom is below the seam, we'll animate our FAB out
                child.hide(mInternalAutoHideListener, false);
            } else {
                // Else, we'll animate our FAB back in
                child.show(mInternalAutoHideListener, false);
            }
            return true;
        }

        private boolean updateFabVisibilityForBottomSheet(View bottomSheet,
                FloatingActionButton child) {
            if (!shouldUpdateVisibility(bottomSheet, child)) {
                return false;
            }
            CoordinatorLayout.LayoutParams lp =
                    (CoordinatorLayout.LayoutParams) child.getLayoutParams();
            if (bottomSheet.getTop() < child.getHeight() / 2 + lp.topMargin) {
                child.hide(mInternalAutoHideListener, false);
            } else {
                child.show(mInternalAutoHideListener, false);
            }
            return true;
        }

可以發現,這兩個類裡面,hide的方法都是傳入false的,還有,從名字可以發現,這兩個方法應該分別是針對appbarlayout和bottomSheet的,訪問許可權是私有的,所以可以繼續在fab類裡面搜尋呼叫的地方,發現這兩個方法,都在另外兩個方法裡面一起被呼叫了

@Override
        public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
                View dependency) {
            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
                updateFabVisibilityForAppBarLayout(parent, (AppBarLayout) dependency, child);
            } else if (isBottomSheet(dependency)) {
                updateFabVisibilityForBottomSheet(dependency, child);
            }
            return false;
        }
@Override
        public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child,
                int layoutDirection) {
            // First, let's make sure that the visibility of the FAB is consistent
            final List<View> dependencies = parent.getDependencies(child);
            for (int i = 0, count = dependencies.size(); i < count; i++) {
                final View dependency = dependencies.get(i);
                if (dependency instanceof AppBarLayout) {
                    if (updateFabVisibilityForAppBarLayout(
                            parent, (AppBarLayout) dependency, child)) {
                        break;
                    }
                } else if (isBottomSheet(dependency)) {
                    if (updateFabVisibilityForBottomSheet(dependency, child)) {
                        break;
                    }
                }
            }
            // Now let the CoordinatorLayout lay out the FAB
            parent.onLayoutChild(child, layoutDirection);
            // Now offset it if needed
            offsetIfNeeded(parent, child);
            return true;
        }

也就是說,只有在(AppBarLayout或者BottomSheet)與fab同為兄弟佈局的時候,然後在他們滑動的時候,fab才會正常顯示和隱藏,那麼其他佈局怎麼辦?像RecyclerView這種情況下該怎麼解決?

其實也很簡單,只要不執行hide方法就行了,自己實現隱藏動畫,我這裡直接拿了fab隱藏的動畫,進行了修改了一下,然後就變成了我自己的,接下來直接貼程式碼好了。

繼承FloatingActionButton.Behavior,另外實現了一個類ScrollAwareFABBehavior,程式碼如下

package com.voctex.ui.tablayout.other;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
import android.support.v4.view.animation.LinearOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by mac_xihao on 17/7/3.
 * (~ ̄▽ ̄)~ 嘛哩嘛哩哄
 */
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {


    /**
     * 因為是在XML中使用app:layout_behavior定義靜態的這種行為,
     * 必須實現一個建構函式使佈局的效果能夠正常工作。
     * 否則 Could not inflate Behavior subclass error messages.
     *
     * @param context
     * @param attrs
     */
    public ScrollAwareFABBehavior(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 > 10 && child.getVisibility() == View.VISIBLE) {
            // User scrolled down and the FAB is currently visible -> hide the FAB
            //執行隱藏的動畫
            hide(child);
        } else if (dyConsumed < -10 && child.getVisibility() != View.VISIBLE) {
            // User scrolled up and the FAB is currently not visible -> show the FAB
            //執行顯示的動畫
            show(child);
        }

    }

    /**
     * 顯示的動畫
     */
    private void show(final View view) {
        view.animate().cancel();

        // If the view isn't visible currently, we'll animate it from a single pixel
        view.setAlpha(0f);
        view.setScaleY(0f);
        view.setScaleX(0f);

        view.animate()
                .scaleX(1f)
                .scaleY(1f)
                .alpha(1f)
                .setDuration(200)
                .setInterpolator(new LinearOutSlowInInterpolator())
                .setListener(new AnimatorListenerAdapter() {

                    @Override
                    public void onAnimationStart(Animator animation) {
                        view.setVisibility(View.VISIBLE);
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {

                    }
                });
    }

    /**
     * 隱藏的動畫
     */
    private void hide(final View view) {
        view.animate().cancel();
        view.animate()
                .scaleX(0f)
                .scaleY(0f)
                .alpha(0f)
                .setDuration(200)
                .setInterpolator(new FastOutLinearInInterpolator())
                .setListener(new AnimatorListenerAdapter() {
                    private boolean mCancelled;

                    @Override
                    public void onAnimationStart(Animator animation) {
                        view.setVisibility(View.VISIBLE);
                        mCancelled = false;
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {
                        mCancelled = true;
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        if (!mCancelled) {
                            view.setVisibility(View.INVISIBLE);
                        }
                    }
                });
    }
}

然後在xml那裡直接改成

app:layout_behavior="com.voctex.ui.tablayout.other.ScrollAwareFABBehavior"

發現看原始碼還是一件比較有趣的事,就是有點耗時間。

如果想仔細檢視所有程式碼的話,可以直接導下我的專案自己執行,並測試。

QQ:361561789
有事可以直接加Q聯絡