本文已授权微信公众号”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