FloatingActionButton基本使用及踩坑记录

  • Post author:
  • Post category:其他



本文已授权微信公众号”Android技术杂货铺”发布。

FloatingActionButton(FAB)是 Android 5.0 新特性——Material Design中的一个控件。FloatingActionButton其实由3个单词组成, Floating:悬浮;Action:行为,Button:按钮。的确,FAB就是一个悬浮的按钮。

本文将结合笔者的开发经验,通过FAB的基本使用,使用过程中遇到的问题及解决方案等方面进行阐述,以便大家进一步认识FAB。

另外,本文若无特别说明,源码分析均采用27.1.0。



一.基本使用

1.FAB是Material Design (以下简称MD)中的一个控件.跟所有MD控件一样,要使用FAB,需要在gradle文件中先注册依赖:

implementation 'com.android.support:design:27.1.0'

2.FAB的基本使用

通过查看源码可知,FAB是 ImageView 的子类,因此它具备ImageView的全部属性。如果你只是进行最简单的操作,代码如下:

     <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/fab_up"
        />

运行效果图如下:

这里写图片描述

可以看到我们的FloatingActionButton正常显示的情况下有个填充的颜色,有个阴影;点击的时候会有一个rippleColor,并且阴影的范围可以增大,那么问题来了:

  • 这个填充色以及rippleColor如何自定义呢?

默认的颜色取的是theme中的colorAccent,所以你可以在style中定义colorAccent。

这里写图片描述

rippleColor默认取的是theme中的colorControlHighlight。

我们也可以直接用过属性定义这两个的颜色:

  • 立体感有没有什么属性可以动态指定?

和立体感相关有两个属性,elevation和pressedTranslationZ,前者用户设置正常显示的阴影大小;后者是点击时显示的阴影大小。大家可以自己设置尝试下。

通过上述描述可知:如果你想默认的颜色、点击后颜色,不需要单独设置backgroundTint、rippleColor属性。如果需要自定义,直接根据UI设计提供的颜色即可.

其中比较常见的用法代码如下:

 <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:src="@drawable/fab_up"
        app:backgroundTint="#FFFFFF"
        app:borderWidth="0dp"
        app:elevation="5dp"
        app:fabSize="mini"
        app:layout_anchor="@id/cl"
        app:layout_anchorGravity="bottom|right|end"
        app:pressedTranslationZ="10dp"
        app:rippleColor="#a6a6a6"
        />

现在,我们来总结一下属性说明:

android:src:FAB中显示的图标.  
app:backgroundTint:正常的背景颜色  
app:rippleColor:按下时的背景颜色  
app:elevation:正常的阴影大小  
app:pressedTranslationZ:按下时的阴影大小  
app:layout_anchor:设置FAB的锚点,即以哪个控件为参照设置位置  
app:layout_anchorGravity:FAB相对于锚点的位置  
app:fabSize:FAB的大小,normal或mini(分别对应56dp和40dp)  

其中,有几点需要特别注意:

1.要想让FAB显示点击后的颜色和阴影变化效果,必须设置onClick事件。

2.上述的app:layout_anchor,父类布局使用FrameLayout是没有效果的,需要使用加强版的FrameLayout即CoordinatorLayout。参考锚点不能以父类为参考,要不然会报错:java.lang.IllegalStateException: View can not be anchored to the the parent CoordinatorLayout。

正确的xml代码如下(TextView可以换成其他View):

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

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:src="@drawable/fab_up"
        app:backgroundTint="#FFFFFF"
        app:borderWidth="0dp"
        app:elevation="5dp"
        app:fabSize="mini"
        app:layout_anchor="@id/tv"
        app:layout_anchorGravity="bottom|right|end"
        app:pressedTranslationZ="10dp"
        app:rippleColor="#a6a6a6"
        />
</android.support.design.widget.CoordinatorLayout>

当然,FAB的位置,在这里您不使用CoordinatorLayout,而是直接使用FrameLayout,同时通过android:layout_gravity=”bottom|right”属性确定FAB的位,在这里是可以的。但,不建议这样使用。为什么呢?等你看完下文”与SnackBar结合使用”您就明白了。

至于FAB的交互,由于它是ImageView的子类,直接 mFab.setOnClickListener() 即可。



二.5.x存在的一些问题

在5.x的设备上运行,你会发现一些问题(测试系统5.0):

  • 没有阴影

记得设置app:borderWidth=“0dp”。

  • 按上述设置后,阴影出现了,但是竟然有矩形的边界(未设置margin时,可以看出)

需要设置一个margin的值。在5.0之前,会默认就有一个外边距(不过并非是margin,只是效果相同)。

因此,比较好的解决方案是:

  • 添加属性app:borderWidth=“0dp”
  • 对于5.x设置一个合理的margin。代码如下:

    这里写图片描述

然后:

values

 <dimen name="fab_margin">0dp</dimen>

values-v21

<dimen name="fab_margin">16dp</dimen>



三.高级使用##



(一)与recyclerView结合使用##

####1.使用##

很多时候,fab在界面中扮演的角色,是辅助按钮,比如点击后刷新数据、将页面滚动到最上面.这种情况,在recyclerView中进行出现。

那么问题来了,如果fab扮演的是 点击按钮后,页面滚动到最上面。当用户在往下滑动时,希望fab不要出现.往上滑动时,才出现fab按钮.

这是时候,单纯通过监听recyclerView滚动 显示或隐藏 fab 的确是一种解决方案.那,有没有更优雅的方式呢?

通过阅读FAB的源码我们可以发现,有这样一个类

class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton>

没错,FAB与其他的MD控件一样,谷歌的工程师早已经对FAB与其他控件的行为进行了封装.如果我们要完成FAB在recyclerView滚动时的隐藏、显示,我们只需要集成 FloatingActionButton.Behavior即可。

代码如下:

public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {

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

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton
            child, View directTargetChild, View target, int nestedScrollAxes) {
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
                super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
                        nestedScrollAxes);
    }

    @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) {
            child.hide();     
        } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
            child.show();
        }
    }
}

其中核心逻辑就是onNestedScroll方法中的判断。

然后,我们在xml中引入ScrollAwareFABBehavior 的路径即可

app:layout_behavior=“com.glh.fabdemo.ScrollAwareFABBehavior”

一定要引入正确,要不然会报错。报错信息类似如下:

这里写图片描述

####2.存在的坑##

上述使用,我之前一直没有出现问题,当我把将MD依赖库版本进行升级后,发现:

fab只会隐藏,隐藏后就不出现了.

果不其然,查找原因后才发现,SDK在25及以上的时候,出现了只能隐藏不能重新出现的问题(24及以下没有出现此问题)。

没办法,只有对比看源码.通过进入CoordinatorLayout源码里面看了下,在该类的onNestedScroll()方法中对比24版本和25版本的SDK,发现25多了一点代码:

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

        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            if (view.getVisibility() == GONE) {     //这个判断就是比24多出的  
                // If the child is GONE, skip...
                continue;
            }

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

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

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

就是因为多了上面那一行判断,而我们在自定义中Behavior中hide()的时候,将FloatingActionButton隐藏了,导致代码执行到上述标红部分的时候,就直接跳出了for循环,那就没法回调onNestedScroll()方法了。

####3.解决方案##

将自定义的ScrollAwareFABBehavior中的

child.hide(); 改成 setVisibility(INVISIBLE)

,或者将其移除屏幕显示范围来达到隐藏的效果 (并且,使用setVisibility(GONE)也是无法实现的)



(二)自定义背景##

我们公司的UI是白色控,她的设计风格就是白色。结果item的背景设计为白色,fab按钮背景也设计成白色。由于都是白色,为了区分,她在设计fab时,在fab上加一个灰色的边框。

####1.尝试一

当时我的第一反应是既然fab是imageView的子类,我听过shape画一个有灰色边框,然后用白色填充的图像,然后bacbackground引用这个shape就行.然后我运行代码,发现没用.

这里写图片描述

这里写图片描述

然后我在网上搜索,有的帖子说直接在xml中写没用,需要在代码中写.我再次尝试,还是没效.没办法,我点进fab的源码看。相关代码如下:

@Override
    public void setBackgroundDrawable(Drawable background) {
        Log.i(LOG_TAG, "Setting a custom background is not supported.");
    }

    @Override
    public void setBackgroundResource(int resid) {
        Log.i(LOG_TAG, "Setting a custom background is not supported.");
    }

    @Override
    public void setBackgroundColor(int color) {
        Log.i(LOG_TAG, "Setting a custom background is not supported.");
    }

    @Override
    public void setImageResource(@DrawableRes int resId) {
        // Intercept this call and instead retrieve the Drawable via the image helper
        mImageHelper.setImageResource(resId);
    }

上述的setBackgroundDrawable、setBackgroundResource、setBackgroundColor什么都没干。说白一点,设置这个属性没用。而setImageResource代码是设置icon资源的。通过尝试后,发现的确如此。

fab不是还有一个backgroundTint属性吗?那我们看相关源码:

 /**
     * Returns the tint applied to the background drawable, if specified.
     *
     * @return the tint applied to the background drawable
     * @see #setBackgroundTintList(ColorStateList)
     */
    @Nullable
    @Override
    public ColorStateList getBackgroundTintList() {
        return mBackgroundTint;
    }

    /**
     * Applies a tint to the background drawable. Does not modify the current tint
     * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
     *
     * @param tint the tint to apply, may be {@code null} to clear tint
     */
    @Override
    public void setBackgroundTintList(@Nullable ColorStateList tint) {
        if (mBackgroundTint != tint) {
            mBackgroundTint = tint;
            getImpl().setBackgroundTintList(tint);
        }
    }

发现backgroundTint属性压根就不支持shape类型的资源。

####2.尝试二

既然fab不能像其他控件那样使用shape资源.现在又需要带边框的悬浮按钮,然后我让公司的UI设计师涉及一个带灰色边框的icon,然后引用这个icon总可以吧。

运行工程,发现有2个边框。一个是icon的边框,一个是fab自带的边框。

这里写图片描述

这里写图片描述

原来fab边框与icon之间有一个默认的内边距. 我通过尝试设置padding为0或者负数,都没有效果.再点源码进行查看,也没发现相关方法。

或许你会问:fab不是自带有阴影效果吗?阴影效果其实就是默认的灰色边框。

其实我在花费了差不多一天的时候后,也是对UI这么说的。但她要求必须与设计图一模一样(也就是不能要阴影)。但问题是,如果不要我阴影这个属性,白色的item与白色的fab就完成看不到了(fab的边框弄不出来)。

当时,我真的准备放弃了,准备换一个普通的图片,以替代fab。

####3.成功的尝试

那天晚上,我重温《android群英传》的自定义组合控件.。当时灵光一现,我之前所有的思路是如何通过添加背景而实现带灰色边框。

既然行不通,我为什么不自定义fab呢,继承官方的fab后重写onDraw()方法,通过重写的形式,add边框上去。

一尝试,果然有效:

代码如下:

public class AddBorderFab extends FloatingActionButton {

    private static final int borderWidth = 1;    //边框的宽度

    Paint  paint;
    Canvas canvas;

    public AddBorderFab(Context context) {
        super(context);
        initView();
    }

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

    public AddBorderFab(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        canvas = new Canvas();
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(Color.parseColor("#c0c3c5"));
        paint.setStrokeWidth((float) borderWidth);
        paint.setStyle(Paint.Style.STROKE);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, getMeasuredWidth() / 2
                - borderWidth, paint);

        canvas.save();
        super.onDraw(canvas);
        canvas.restore();
    }
}

效果如下:

这里写图片描述

如何画控件,不属于本文谈论的范畴。这里略过。



(三)与SnackBar结合使用##

SnackBar作为另外一个MD的小工具,它比较类似Toast。但需要明确 Snackbar 并不是 Toast 的替代品,应用场景是不一样的,Toast 只是一个提示作用,用户并不能进行操作,而 Snackbar 则不同,它允许在提示当中加入一个交互按钮,当用户点击的时候可以进行一写相应额逻辑操作,相应的提升了用户体验。

如果您对SnackBar还不太了解,还真的需要好好学习一下MD控件了。

由于SnackBar出现的位置位于界面的底部,而FAB显示出来时,很多时候位于界面的右下角。如果需求是点击FAB后出现SnackBar的提示,按照一般的布局写法(比如FrameLayout),运行工程后,如下图所示:

这里写图片描述

注意到没: Snackbar 从底部弹出以后遮挡了 FlaotingActionButton 按钮。这样的体验效果当然非常不好。

如果将FrameLayout改成加强版的FrameLayout—>>CoordinatorLayout后呢?我们一起看看运行的效果:

这里写图片描述

FAB在SnackBar出现时,自己自动的往上”跑了一段距离”,等SnackBar消失后,FAB又自动的”沉”到原来的位置了。

看到这里,你应该明白了为什么在”一.基本使用 “的最后,我建议您使用CoordinatorLayout而非FrameLayout了吧。

而CoordinatorLayout也是作为MD的控件,谷歌工程师在设计MD时,充分考虑到了这一件事,并对控件行为进行了处理。



四.最后说几句##

1.fab的拓展性其实不太好,我之前在写开源项目中还没有发现,毕竟开源项目使用控件比较随意,但公司的商业项目有可能扣图很死,很多时候不允许有一点点不同。尤其是当你遇到一位其实不懂app设计的UI时—无论你怎么解释,她不会听的。

2.其实在谷歌官方推出fab之前,GitHub上已经有了很多具备fab功能的开源项目,你直接搜索FloatingActionButton就会发现它们的拓展性比官方的好很多。需求开发上,也更可能符合你项目的需求。

比如这几篇:



源码:


源码(点击跳转)


关于我:

1.一个热爱Android编程,也乐于分享、交友的菜鸟。

我的QQ:984992087

2.

GitHub地址.点击跳转



版权声明:本文为gaolh89原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。