本文已授权微信公众号”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
ColorStateListgetBackgroundTintList() { 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就会发现它们的拓展性比官方的好很多。需求开发上,也更可能符合你项目的需求。

比如这几篇:

* FloatingActionButton <https://github.com/Clans/FloatingActionButton>
* FloatingActionButton <https://github.com/makovkastar/FloatingActionButton>
源码:

源码(点击跳转) <https://github.com/gaolhjy/Blog_SourceCode>

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

2.GitHub地址.点击跳转 <https://github.com/gaolhjy>

友情链接
ioDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信