揭秘 Android FloatingActionButton:从入门到源码深度剖析
一、引言
在当今的 Android 应用开发领域,用户界面的设计与交互体验愈发重要。为了给用户带来更加便捷、美观且高效的操作感受,众多具有特色的 UI 组件应运而生。其中,FloatingActionButton(FAB)作为 Material Design 设计语言中的重要元素,以其独特的悬浮式设计和简洁直观的交互方式,在各类应用中得到了广泛的应用。
FloatingActionButton 通常以一个圆形的图标按钮形式呈现,悬浮在应用界面的特定位置,用于突出显示应用的主要操作或常用功能。它不仅能够吸引用户的注意力,还能让用户快速地执行关键操作,从而提升应用的整体易用性和用户体验。
本文将深入剖析 Android FloatingActionButton 的使用原理,从基础的使用方法入手,逐步深入到源码级别,详细解读其内部的实现机制,包括构造函数、属性设置、测量与布局、绘制过程、点击事件处理以及动画效果实现等方面。通过对源码的分析,开发者可以更好地理解 FloatingActionButton 的工作原理,从而在实际开发中更加灵活地运用它,创造出更加出色的用户界面。
二、FloatingActionButton 概述
2.1 基本概念
FloatingActionButton 是 Android 支持库中提供的一个视图组件,它继承自 ImageButton
类,因此具备 ImageButton
的基本特性,同时又融入了 Material Design 的设计风格和动画效果。它通常用于显示应用的主要操作,如新建、分享、编辑等,通过悬浮在界面上的方式,方便用户快速访问这些操作。
2.2 主要作用
- 突出主要操作:FloatingActionButton 以醒目的圆形图标和悬浮的方式显示在界面上,能够吸引用户的注意力,让用户快速识别和执行应用的主要操作。
- 提升用户体验:通过提供便捷的操作入口,减少用户的操作步骤,提高应用的使用效率,从而提升用户体验。
- 增强界面美观度:FloatingActionButton 的圆形设计和动画效果符合 Material Design 的美学原则,能够为应用界面增添现代感和时尚感。
2.3 继承关系
FloatingActionButton 继承自 ImageButton
类,而 ImageButton
又继承自 ImageView
类,最终继承自 View
类。以下是其继承关系的简单示意:
java
// FloatingActionButton 继承自 ImageButton
public class FloatingActionButton extends ImageButton {
// 类的具体实现
}
// ImageButton 继承自 ImageView
public class ImageButton extends ImageView {
// 类的具体实现
}
// ImageView 继承自 View
public class ImageView extends View {
// 类的具体实现
}
2.4 构造方法
FloatingActionButton 提供了多个构造方法,用于在不同的场景下创建实例。下面详细介绍这些构造方法:
2.4.1 双参数构造方法
java
// 双参数构造方法,用于在代码中创建 FloatingActionButton 实例
public FloatingActionButton(Context context, AttributeSet attrs) {
// 调用三参数构造方法,传入上下文、属性集和默认样式属性
this(context, attrs, R.attr.floatingActionButtonStyle);
}
在这个构造方法中,接收 Context
和 AttributeSet
作为参数。Context
是应用程序的上下文对象,用于获取资源和执行操作;AttributeSet
是 XML 布局文件中定义的属性集合。通过调用三参数构造方法,并传入默认的样式属性 R.attr.floatingActionButtonStyle
,来完成实例的初始化。
2.4.2 三参数构造方法
java
// 三参数构造方法,用于在代码中创建 FloatingActionButton 实例,并指定默认样式属性
public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
// 调用父类的构造方法,传入上下文、属性集和默认样式属性
super(context, attrs, defStyleAttr);
// 初始化 FloatingActionButton 的内部状态
initialize(context, attrs, defStyleAttr);
}
在这个构造方法中,除了接收 Context
和 AttributeSet
外,还接收一个 defStyleAttr
参数,用于指定默认的样式属性。首先调用父类的构造方法,将这些参数传递给父类进行初始化。然后调用 initialize
方法,对 FloatingActionButton 的内部状态进行初始化。
2.4.3 初始化方法
java
// 初始化方法,用于初始化 FloatingActionButton 的内部状态
private void initialize(Context context, AttributeSet attrs, int defStyleAttr) {
// 创建一个 TypedArray 对象,用于获取属性值
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FloatingActionButton, defStyleAttr, 0);
// 获取背景颜色属性
ColorStateList backgroundTint = a.getColorStateList(R.styleable.FloatingActionButton_backgroundTint);
if (backgroundTint != null) {
// 如果背景颜色属性不为空,设置背景颜色
setBackgroundTintList(backgroundTint);
}
// 获取图标属性
Drawable src = a.getDrawable(R.styleable.FloatingActionButton_src);
if (src != null) {
// 如果图标属性不为空,设置图标
setImageDrawable(src);
}
// 获取大小属性
int size = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_NORMAL);
setSize(size);
// 回收 TypedArray 对象,释放资源
a.recycle();
// 初始化阴影效果
initElevation();
// 初始化点击动画效果
initClickAnimation();
}
在 initialize
方法中,首先通过 context.obtainStyledAttributes
方法获取一个 TypedArray
对象,用于从属性集合中获取各个属性的值。然后分别获取背景颜色、图标、大小等属性,并根据这些属性的值进行相应的设置。接着回收 TypedArray
对象,释放资源。最后调用 initElevation
方法初始化阴影效果,调用 initClickAnimation
方法初始化点击动画效果。
三、XML 资源定义
3.1 基本 XML 结构
在 Android 开发中,通常使用 XML 布局文件来定义 FloatingActionButton。以下是一个简单的 XML 示例:
xml
<!-- res/layout/activity_main.xml -->
<androidx.coordinatorlayout.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">
<!-- 定义一个 FloatingActionButton -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:srcCompat="@drawable/ic_add"
app:backgroundTint="@color/colorAccent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
3.2 标签解释
<androidx.coordinatorlayout.widget.CoordinatorLayout>
:这是一个协调布局容器,用于管理子视图之间的交互和布局。FloatingActionButton 通常需要放置在CoordinatorLayout
中,以便与其他视图进行交互。<com.google.android.material.floatingactionbutton.FloatingActionButton>
:这是 FloatingActionButton 的标签,用于定义一个 FloatingActionButton 实例。android:id
:为 FloatingActionButton 定义一个唯一的标识符,用于在代码中引用该视图。android:layout_width
和android:layout_height
:指定 FloatingActionButton 的宽度和高度。通常使用wrap_content
让视图根据内容自动调整大小。android:layout_gravity
:指定 FloatingActionButton 在父布局中的对齐方式。例如,bottom|end
表示将按钮对齐到布局的右下角。android:layout_margin
:指定 FloatingActionButton 与周围视图的边距。app:srcCompat
:指定 FloatingActionButton 显示的图标。使用srcCompat
可以兼容不同版本的 Android 系统。app:backgroundTint
:指定 FloatingActionButton 的背景颜色。
3.3 从 XML 加载 FloatingActionButton
在 Java 代码中,可以通过 findViewById
方法从 XML 布局文件中加载 FloatingActionButton:
java
// 在 Activity 中获取 FloatingActionButton 实例
FloatingActionButton fab = findViewById(R.id.fab);
// 为 FloatingActionButton 设置点击事件监听器
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 处理点击事件
Toast.makeText(MainActivity.this, "FAB clicked!", Toast.LENGTH_SHORT).show();
}
});
3.4 XML 解析源码分析
当 Android 系统解析 XML 布局文件时,会调用 LayoutInflater
类来创建视图实例。以下是 LayoutInflater
中与解析 FloatingActionButton 相关的部分源码:
java
// 解析 XML 标签并创建视图实例的方法
public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
// 处理一些特殊标签
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// 尝试使用 Factory 或 Factory2 创建视图
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
}
// 如果 Factory 没有创建视图,使用默认的创建方式
if (view == null) {
if (-1 == name.indexOf('.')) {
// 处理系统内置的视图标签
view = onCreateView(parent, name, attrs);
} else {
// 处理自定义的视图标签
view = createView(name, null, attrs);
}
}
return view;
}
在 createViewFromTag
方法中,首先处理一些特殊标签,然后尝试使用 Factory
或 Factory2
创建视图。如果 Factory
没有创建视图,则根据标签名的不同,使用默认的创建方式。对于 FloatingActionButton 这样的自定义视图,会调用 createView
方法来创建实例。
java
// 创建视图实例的方法
public View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// 缓存中查找构造函数
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// 加载类
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
// 获取构造函数
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
if (mFilter != null) {
// 检查类是否允许加载
Class<?> clazzToCheck = constructor.getDeclaringClass();
if (mFilter.onLoadClass(clazzToCheck)) {
failNotAllowed(name, prefix, attrs);
}
}
}
// 创建视图实例
Object[] args = mConstructorArgs;
args[1] = attrs;
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// 处理 ViewStub
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} catch (NoSuchMethodException e) {
// 处理异常
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassCastException e) {
// 处理异常
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (ClassNotFoundException e) {
// 处理异常
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Binary XML file line " + attrs.getLineNumber()
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
// 处理异常
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
在 createView
方法中,首先从缓存中查找构造函数,如果没有找到,则加载类并获取构造函数。然后使用构造函数创建视图实例,并处理一些特殊情况,如 ViewStub
。最后返回创建好的视图实例。
四、属性设置
4.1 背景颜色设置
FloatingActionButton 的背景颜色可以通过 app:backgroundTint
属性在 XML 中设置,也可以通过 setBackgroundTintList
方法在代码中设置。以下是代码示例:
java
// 获取 FloatingActionButton 实例
FloatingActionButton fab = findViewById(R.id.fab);
// 创建一个 ColorStateList 对象,设置背景颜色
ColorStateList colorStateList = ColorStateList.valueOf(Color.RED);
// 设置背景颜色
fab.setBackgroundTintList(colorStateList);
4.2 图标设置
FloatingActionButton 的图标可以通过 app:srcCompat
属性在 XML 中设置,也可以通过 setImageDrawable
方法在代码中设置。以下是代码示例:
java
// 获取 FloatingActionButton 实例
FloatingActionButton fab = findViewById(R.id.fab);
// 获取图标 Drawable 对象
Drawable drawable = getResources().getDrawable(R.drawable.ic_add);
// 设置图标
fab.setImageDrawable(drawable);
4.3 大小设置
FloatingActionButton 有两种大小:正常大小(SIZE_NORMAL
)和迷你大小(SIZE_MINI
)。可以通过 app:fabSize
属性在 XML 中设置,也可以通过 setSize
方法在代码中设置。以下是代码示例:
java
// 获取 FloatingActionButton 实例
FloatingActionButton fab = findViewById(R.id.fab);
// 设置为迷你大小
fab.setSize(FloatingActionButton.SIZE_MINI);
4.4 阴影设置
FloatingActionButton 默认带有阴影效果,可以通过 app:elevation
属性在 XML 中设置阴影的高度,也可以通过 setElevation
方法在代码中设置。以下是代码示例:
java
// 获取 FloatingActionButton 实例
FloatingActionButton fab = findViewById(R.id.fab);
// 设置阴影高度
fab.setElevation(10);
4.5 属性设置源码分析
在 FloatingActionButton
类中,提供了一系列的方法来设置各个属性。以下是部分属性设置方法的源码分析:
4.5.1 setBackgroundTintList
方法
java
// 设置背景颜色的方法
@Override
public void setBackgroundTintList(ColorStateList tint) {
// 调用父类的方法设置背景颜色
super.setBackgroundTintList(tint);
// 更新背景颜色
updateBackgroundTint();
}
在 setBackgroundTintList
方法中,首先调用父类的方法设置背景颜色,然后调用 updateBackgroundTint
方法更新背景颜色。
4.5.2 setImageDrawable
方法
java
// 设置图标的方法
@Override
public void setImageDrawable(Drawable drawable) {
// 调用父类的方法设置图标
super.setImageDrawable(drawable);
// 更新图标
updateImageDrawable();
}
在 setImageDrawable
方法中,首先调用父类的方法设置图标,然后调用 updateImageDrawable
方法更新图标。
4.5.3 setSize
方法
java
// 设置大小的方法
public void setSize(int size) {
if (size != mSize) {
// 检查大小是否合法
if (size != SIZE_NORMAL && size != SIZE_MINI) {
throw new IllegalArgumentException("Invalid size: " + size);
}
// 更新大小
mSize = size;
// 请求重新测量和布局
requestLayout();
}
}
在 setSize
方法中,首先检查传入的大小是否合法,如果合法则更新大小,并调用 requestLayout
方法请求重新测量和布局。
4.5.4 setElevation
方法
java
// 设置阴影高度的方法
@Override
public void setElevation(float elevation) {
// 调用父类的方法设置阴影高度
super.setElevation(elevation);
// 更新阴影效果
updateElevation();
}
在 setElevation
方法中,首先调用父类的方法设置阴影高度,然后调用 updateElevation
方法更新阴影效果。
五、测量与布局
5.1 测量过程
在 Android 系统中,视图的测量过程由 onMeasure
方法完成。FloatingActionButton 重写了 onMeasure
方法,用于确定自身的宽度和高度。以下是 onMeasure
方法的源码:
java
// 测量方法,用于确定视图的宽度和高度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取默认的宽度和高度
int defaultSize = getDefaultSize();
// 解析宽度测量规格
int width = resolveSizeAndState(defaultSize, widthMeasureSpec, 0);
// 解析高度测量规格
int height = resolveSizeAndState(defaultSize, heightMeasureSpec, 0);
// 设置测量结果
setMeasuredDimension(width, height);
}
在 onMeasure
方法中,首先调用 getDefaultSize
方法获取默认的宽度和高度。然后使用 resolveSizeAndState
方法解析宽度和高度的测量规格,得到最终的宽度和高度。最后调用 setMeasuredDimension
方法设置测量结果。
5.1.1 getDefaultSize
方法
java
// 获取默认大小的方法
private int getDefaultSize() {
// 根据大小属性返回不同的默认大小
if (mSize == SIZE_NORMAL) {
return getResources().getDimensionPixelSize(R.dimen.design_fab_size_normal);
} else {
return getResources().getDimensionPixelSize(R.dimen.design_fab_size_mini);
}
}
在 getDefaultSize
方法中,根据 mSize
属性的值返回不同的默认大小。如果是正常大小,则返回 R.dimen.design_fab_size_normal
对应的尺寸;如果是迷你大小,则返回 R.dimen.design_fab_size_mini
对应的尺寸。
5.1.2 resolveSizeAndState
方法
java
// 解析测量规格的方法
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
// 获取测量规格的模式和大小
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
// 如果模式是 AT_MOST,取指定大小和测量规格大小的最小值
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
// 如果模式是 EXACTLY,直接使用测量规格的大小
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
// 如果模式是 UNSPECIFIED,使用指定的大小
result = size;
break;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
在 resolveSizeAndState
方法中,根据测量规格的模式(AT_MOST
、EXACTLY
或 UNSPECIFIED
),确定最终的大小。如果模式是 AT_MOST
,取指定大小和测量规格大小的最小值;如果模式是 EXACTLY
,直接使用测量规格的大小;如果模式是 UNSPECIFIED
,使用指定的大小。
5.2 布局过程
在 Android 系统中,视图的布局过程由 onLayout
方法完成。FloatingActionButton 重写了 onLayout
方法,用于确定自身在父布局中的位置。以下是 onLayout
方法的源码:
java
// 布局方法,用于确定视图在父布局中的位置
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 调用父类的布局方法
super.onLayout(changed, left, top, right, bottom);
// 更新阴影效果
updateElevation();
}
在 onLayout
方法中,首先调用父类的布局方法,完成视图的布局。然后调用 updateElevation
方法更新阴影效果。
5.3 与 CoordinatorLayout 的交互
FloatingActionButton 通常需要放置在 CoordinatorLayout
中,以便与其他视图进行交互。CoordinatorLayout
是一个协调布局容器,它可以监听子视图的滚动事件,并根据滚动事件来控制 FloatingActionButton 的显示和隐藏。以下是一个简单的示例:
xml
<!-- res/layout/activity_main.xml -->
<androidx.coordinatorlayout.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">
<!-- 定义一个 RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- 定义一个 FloatingActionButton -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
app:srcCompat="@drawable/ic_add"
app:backgroundTint="@color/colorAccent"
app:layout_behavior="@string/hide_on_scroll_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
在这个示例中,RecyclerView
是一个可滚动的视图,FloatingActionButton
通过 app:layout_behavior
属性指定了一个 HideOnScrollBehavior
,当 RecyclerView
滚动时,FloatingActionButton
会根据滚动方向自动显示或隐藏。
5.3.1 HideOnScrollBehavior
源码分析
java
// 隐藏在滚动时的行为类
public class HideOnScrollBehavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
// 动画状态:显示
private static final int STATE_SHOWN = 1;
// 动画状态:隐藏
private static final int STATE_HIDDEN = 2;
// 当前动画状态
private int mState = STATE_SHOWN;
// 动画执行器
private ValueAnimator mAnimator;
// 构造方法,用于从 XML 中创建实例
public HideOnScrollBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 处理滚动事件的方法
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull FloatingActionButton child, @NonNull View directTargetChild,
@NonNull View target, int axes, int type) {
// 只处理垂直滚动事件
return axes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
// 处理滚动事件的方法
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull FloatingActionButton child, @NonNull View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type,
@NonNull int[] consumed) {
if (dyConsumed > 0) {
// 向下滚动,隐藏 FloatingActionButton
hide(child);
} else if (dyConsumed < 0) {
// 向上滚动,显示 FloatingActionButton
show(child);
}
}
// 隐藏 FloatingActionButton 的方法
private void hide(final FloatingActionButton fab) {
if (mState == STATE_HIDDEN) {
return;
}
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
mAnimator = ValueAnimator.ofFloat(1f, 0f);
mAnimator.setDuration(200);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
fab.setScaleX(value);
fab.setScaleY(value);
fab.setAlpha(value);
}
});
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mState = STATE_HIDDEN;
}
});
mAnimator.start();
}
// 显示 FloatingActionButton 的方法
private void show(final FloatingActionButton fab) {
if (mState == STATE_SHOWN) {
return;
}
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
mAnimator = ValueAnimator.ofFloat(0f, 1f);
mAnimator.setDuration(200);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
fab.setScaleX(value);
fab.setScaleY(value);
fab.setAlpha(value);
}
});
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mState = STATE_SHOWN;
}
});
mAnimator.start();
}
}
在 HideOnScrollBehavior
类中,onStartNestedScroll
方法用于判断是否处理滚动事件,这里只处理垂直滚动事件。onNestedScroll
方法根据滚动方向调用 hide
或 show
方法来隐藏或显示 FloatingActionButton。hide
和 show
方法使用 ValueAnimator
来实现缩放和透明度的动画效果。
六、绘制过程
6.1 背景绘制
FloatingActionButton 的背景通常是一个圆形的颜色块,可以通过 app:backgroundTint
属性设置背景颜色。在绘制过程中,FloatingActionButton
会根据背景颜色和大小绘制一个圆形的背景。以下是相关的源码分析:
6.1.1 drawableStateChanged
方法
java
// 当视图的状态发生变化时调用的方法
@Override
protected void drawableStateChanged() {
// 调用父类的方法
super.drawableStateChanged();
// 更新背景状态
updateBackgroundTint();
}
在 drawableStateChanged
方法中,当视图的状态发生变化时,调用 updateBackgroundTint
方法更新背景状态。
6.1.2 updateBackgroundTint
方法
java
// 更新背景颜色的方法
private void updateBackgroundTint() {
// 获取背景 Drawable 对象
Drawable background = getBackground();
if (background != null) {
// 获取背景颜色状态列表
ColorStateList tintList = getBackgroundTintList();
if (tintList != null) {
// 设置背景颜色
DrawableCompat.setTintList(background, tintList);
}
}
}
在 updateBackgroundTint
方法中,首先获取背景 Drawable
对象,然后获取背景颜色状态列表。如果颜色状态列表不为空,则使用 DrawableCompat.setTintList
方法设置背景颜色。
6.2 图标绘制
FloatingActionButton 的图标可以通过 app:srcCompat
属性设置,也可以通过 setImageDrawable
方法在代码中设置。在绘制过程中,FloatingActionButton
会将图标绘制在背景之上。以下是相关的源码分析:
6.2.1 onDraw
方法
java
// 绘制方法,用于绘制视图的内容
@Override
protected void onDraw(Canvas canvas) {
// 调用父类的绘制方法,绘制背景
super.onDraw(canvas);
// 绘制图标
drawIcon(canvas);
}
在 onDraw
方法中,首先调用父类的绘制方法,绘制背景。然后调用 drawIcon
方法绘制图标。
6.2.2 drawIcon
方法
java
// 绘制图标的方法
private void drawIcon(Canvas canvas) {
// 获取图标 Drawable 对象
Drawable drawable = getDrawable();
if (drawable != null) {
// 获取图标的边界
Rect bounds = drawable.getBounds();
// 计算图标的中心位置
int centerX = getWidth() / 2;
int centerY = getHeight() / 2;
// 计算图标的偏移量
int left = centerX - bounds.width() / 2;
int top = centerY - bounds.height() / 2;
// 设置图标的位置
drawable.setBounds(left, top, left + bounds.width(), top + bounds.height());
// 绘制图标
drawable.draw(canvas);
}
}
在 drawIcon
方法中,首先获取图标 Drawable
对象,然后获取图标的边界。计算图标的中心位置和偏移量,设置图标的位置,最后绘制图标。
6.3 阴影绘制
FloatingActionButton 默认带有阴影效果,可以通过 app:elevation
属性设置阴影的高度。在绘制过程中,FloatingActionButton
会根据阴影高度和形状绘制阴影。以下是相关的源码分析:
6.3.1 initElevation
方法
java
// 初始化阴影效果的方法
private void initElevation() {
// 获取默认的阴影高度
float defaultElevation = getResources().getDimension(R.dimen.design_fab_elevation);
// 设置阴影高度
setElevation(defaultElevation);
// 更新阴影效果
updateElevation();
}
在 initElevation
方法中,首先获取默认的阴影高度,然后设置阴影高度。最后调用 updateElevation
方法更新阴影效果。
6.3.2 updateElevation
方法
java
// 更新阴影效果的方法
private void updateElevation() {
// 获取阴影高度
float elevation = getElevation();
// 获取背景 Drawable 对象
Drawable background = getBackground();
if (background instanceof MaterialShapeDrawable) {
// 如果背景是 MaterialShapeDrawable,设置阴影高度
((MaterialShapeDrawable) background).setElevation(elevation);
}
}
在 updateElevation
方法中,首先获取阴影高度,然后获取背景 Drawable
对象。如果背景是 MaterialShapeDrawable
,则设置阴影高度。
七、点击事件处理
7.1 点击事件监听器设置
可以通过 setOnClickListener
方法为 FloatingActionButton 设置点击事件监听器。以下是代码示例:
java
// 获取 FloatingActionButton 实例
FloatingActionButton fab = findViewById(R.id.fab);
// 为 FloatingActionButton 设置点击事件监听器
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 处理点击事件
Toast.makeText(MainActivity.this, "FAB clicked!", Toast.LENGTH_SHORT).show();
}
});
7.2 点击动画效果
FloatingActionButton 在点击时会有一个动画效果,通常是一个水波纹扩散的效果。这个效果是通过 RippleDrawable
实现的。以下是相关的源码分析:
7.2.1 initClickAnimation
方法
java
// 初始化点击动画效果的方法
private void initClickAnimation() {
// 创建一个 RippleDrawable 对象
RippleDrawable rippleDrawable = createRippleDrawable();
// 设置背景为 RippleDrawable
setBackground(rippleDrawable);
}
在 initClickAnimation
方法中,调用 createRippleDrawable
方法创建一个 RippleDrawable
对象,然后将其设置为背景。
7.2.2 createRippleDrawable
方法
java
// 创建 RippleDrawable 对象的方法
private RippleDrawable createRippleDrawable() {
// 获取背景颜色状态列表
ColorStateList backgroundTint = getBackgroundTintList();
if (backgroundTint == null) {
// 如果背景颜色状态列表为空,使用默认颜色
backgroundTint = ColorStateList.valueOf(Color.WHITE);
}
// 创建一个 RippleDrawable 对象
RippleDrawable rippleDrawable = new RippleDrawable(backgroundTint, null, null);
return rippleDrawable;
}
在 createRippleDrawable
方法中,首先获取背景颜色状态列表。如果列表为空,则使用默认颜色。然后创建一个
7.2.3 RippleDrawable
工作原理
RippleDrawable
是 Android 中用于实现水波纹效果的 Drawable
类。当用户点击 FloatingActionButton
时,RippleDrawable
会在点击位置创建一个水波纹,并使其逐渐扩散。下面详细分析其工作原理。
7.2.3.1 水波纹的创建
当用户点击 FloatingActionButton
时,RippleDrawable
会在 onTouchEvent
方法中检测到点击事件,并调用 createRipple
方法创建一个水波纹。以下是简化后的 onTouchEvent
方法和 createRipple
方法的源码分析:
java
// RippleDrawable 的 onTouchEvent 方法,处理触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 当手指按下时,创建水波纹
createRipple(event.getX(), event.getY());
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 当手指抬起或取消事件时,结束水波纹动画
endRipple();
break;
}
return super.onTouchEvent(event);
}
// 创建水波纹的方法
private void createRipple(float x, float y) {
// 创建一个 Ripple 对象,表示一个水波纹
Ripple ripple = new Ripple(this, x, y);
// 将水波纹添加到列表中
mRipples.add(ripple);
// 启动水波纹动画
ripple.start();
}
在 onTouchEvent
方法中,当检测到 ACTION_DOWN
事件时,调用 createRipple
方法。createRipple
方法创建一个 Ripple
对象,并将其添加到 mRipples
列表中,然后启动水波纹动画。
7.2.3.2 水波纹的动画实现
Ripple
类负责实现水波纹的动画效果。它通过不断更新水波纹的半径和透明度,实现水波纹的扩散和消失效果。以下是 Ripple
类的部分源码分析:
java
// Ripple 类,表示一个水波纹
private static class Ripple {
private final RippleDrawable mOwner;
private final float mX;
private final float mY;
private float mRadius;
private int mAlpha;
private long mStartTime;
private long mDuration;
private int mState;
private static final int STATE_STARTED = 1;
private static final int STATE_FINISHED = 2;
// 构造方法,初始化水波纹的参数
public Ripple(RippleDrawable owner, float x, float y) {
mOwner = owner;
mX = x;
mY = y;
mRadius = 0;
mAlpha = 255;
mStartTime = System.currentTimeMillis();
mDuration = 300; // 动画持续时间
mState = STATE_STARTED;
}
// 启动水波纹动画
public void start() {
// 触发重绘,开始动画循环
mOwner.invalidateSelf();
}
// 更新水波纹的状态
public void update(long now) {
if (mState == STATE_FINISHED) {
return;
}
// 计算动画进度
float progress = (now - mStartTime) / (float) mDuration;
if (progress < 1) {
// 根据进度更新半径和透明度
mRadius = mMaxRadius * progress;
mAlpha = (int) (255 * (1 - progress));
} else {
// 动画结束
mState = STATE_FINISHED;
}
// 触发重绘,更新界面
mOwner.invalidateSelf();
}
// 绘制水波纹
public void draw(Canvas canvas) {
if (mState == STATE_FINISHED) {
return;
}
// 设置画笔的颜色和透明度
mPaint.setColor(mOwner.getColor());
mPaint.setAlpha(mAlpha);
// 绘制圆形水波纹
canvas.drawCircle(mX, mY, mRadius, mPaint);
}
}
在 Ripple
类中,start
方法触发重绘,开始动画循环。update
方法根据时间计算动画进度,并更新水波纹的半径和透明度。当动画进度达到 1 时,将水波纹状态设置为 STATE_FINISHED
。draw
方法根据水波纹的当前状态和参数,在 Canvas
上绘制圆形水波纹。
7.3 长按事件处理
除了点击事件,FloatingActionButton
还支持长按事件。可以通过 setOnLongClickListener
方法为其设置长按事件监听器。以下是代码示例:
java
// 获取 FloatingActionButton 实例
FloatingActionButton fab = findViewById(R.id.fab);
// 为 FloatingActionButton 设置长按事件监听器
fab.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
// 处理长按事件
Toast.makeText(MainActivity.this, "FAB long clicked!", Toast.LENGTH_SHORT).show();
return true; // 返回 true 表示事件已处理
}
});
在 onLongClick
方法中,处理长按事件,并返回 true
表示事件已处理。如果返回 false
,则长按事件可能会继续传递给其他监听器。
7.4 点击事件源码整体流程
FloatingActionButton
的点击事件处理整体流程如下:
- 事件分发 :当用户触摸屏幕时,事件会从
Activity
开始,经过ViewGroup
层层传递,最终到达FloatingActionButton
。FloatingActionButton
会调用onTouchEvent
方法处理触摸事件。 - 点击事件检测 :在
onTouchEvent
方法中,检测ACTION_DOWN
和ACTION_UP
事件。当ACTION_DOWN
事件发生时,记录点击位置,并可能触发水波纹动画;当ACTION_UP
事件发生时,判断点击位置是否在按钮范围内,如果是,则认为是一次有效点击。 - 监听器回调 :如果检测到有效点击,会调用设置的
OnClickListener
的onClick
方法;如果检测到长按事件,会调用设置的OnLongClickListener
的onLongClick
方法。
以下是简化后的 FloatingActionButton
的 onTouchEvent
方法源码:
java
// FloatingActionButton 的 onTouchEvent 方法,处理触摸事件
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 记录点击位置
mLastTouchX = event.getX();
mLastTouchY = event.getY();
// 触发水波纹动画
if (mRippleDrawable != null) {
mRippleDrawable.createRipple(mLastTouchX, mLastTouchY);
}
// 启动长按检测
startLongPressTimer();
break;
case MotionEvent.ACTION_UP:
// 停止长按检测
stopLongPressTimer();
// 判断是否为有效点击
if (isInsideButton(mLastTouchX, mLastTouchY)) {
// 调用点击事件监听器
if (mOnClickListener != null) {
mOnClickListener.onClick(this);
}
}
// 结束水波纹动画
if (mRippleDrawable != null) {
mRippleDrawable.endRipple();
}
break;
case MotionEvent.ACTION_CANCEL:
// 停止长按检测
stopLongPressTimer();
// 结束水波纹动画
if (mRippleDrawable != null) {
mRippleDrawable.endRipple();
}
break;
}
return super.onTouchEvent(event);
}
// 启动长按检测定时器
private void startLongPressTimer() {
if (mLongPressTimer != null) {
mLongPressTimer.cancel();
}
mLongPressTimer = new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// 触发长按事件
if (mOnLongClickListener != null) {
mOnLongClickListener.onLongClick(FloatingActionButton.this);
}
}
}, ViewConfiguration.getLongPressTimeout());
}
// 停止长按检测定时器
private void stopLongPressTimer() {
if (mLongPressTimer != null) {
mLongPressTimer.cancel();
mLongPressTimer = null;
}
}
// 判断点击位置是否在按钮范围内
private boolean isInsideButton(float x, float y) {
Rect bounds = new Rect();
getHitRect(bounds);
return bounds.contains((int) x, (int) y);
}
在这个流程中,startLongPressTimer
方法启动一个定时器,当定时器超时后触发长按事件。stopLongPressTimer
方法用于停止定时器。isInsideButton
方法用于判断点击位置是否在按钮范围内。
八、动画效果实现
8.1 显示与隐藏动画
FloatingActionButton
可以实现显示和隐藏的动画效果,通常在与 CoordinatorLayout
配合使用时,根据滚动事件进行显示和隐藏。常见的动画效果有缩放动画和淡入淡出动画。
8.1.1 缩放动画实现
缩放动画通过改变 FloatingActionButton
的缩放比例来实现。可以使用 ValueAnimator
来控制缩放比例的变化。以下是实现缩放动画的代码示例:
java
// 显示 FloatingActionButton 的缩放动画
private void showFabWithScaleAnimation(FloatingActionButton fab) {
// 创建一个从 0 到 1 的值动画
ValueAnimator scaleAnimator = ValueAnimator.ofFloat(0f, 1f);
scaleAnimator.setDuration(300); // 动画持续时间
scaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 获取当前动画值
float value = (float) animation.getAnimatedValue();
// 设置缩放比例
fab.setScaleX(value);
fab.setScaleY(value);
}
});
scaleAnimator.start(); // 启动动画
}
// 隐藏 FloatingActionButton 的缩放动画
private void hideFabWithScaleAnimation(FloatingActionButton fab) {
// 创建一个从 1 到 0 的值动画
ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, 0f);
scaleAnimator.setDuration(300); // 动画持续时间
scaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 获取当前动画值
float value = (float) animation.getAnimatedValue();
// 设置缩放比例
fab.setScaleX(value);
fab.setScaleY(value);
}
});
scaleAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// 动画结束后隐藏按钮
fab.setVisibility(View.GONE);
}
});
scaleAnimator.start(); // 启动动画
}
在 showFabWithScaleAnimation
方法中,创建一个从 0 到 1 的 ValueAnimator
,并在 onAnimationUpdate
方法中更新 FloatingActionButton
的缩放比例。在 hideFabWithScaleAnimation
方法中,创建一个从 1 到 0 的 ValueAnimator
,并在动画结束后将按钮的可见性设置为 GONE
。
8.1.2 淡入淡出动画实现
淡入淡出动画通过改变 FloatingActionButton
的透明度来实现。同样可以使用 ValueAnimator
来控制透明度的变化。以下是实现淡入淡出动画的代码示例:
java
// 显示 FloatingActionButton 的淡入动画
private void showFabWithFadeAnimation(FloatingActionButton fab) {
// 创建一个从 0 到 1 的值动画
ValueAnimator fadeAnimator = ValueAnimator.ofFloat(0f, 1f);
fadeAnimator.setDuration(300); // 动画持续时间
fadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 获取当前动画值
float value = (float) animation.getAnimatedValue();
// 设置透明度
fab.setAlpha(value);
}
});
fadeAnimator.start(); // 启动动画
}
// 隐藏 FloatingActionButton 的淡出动画
private void hideFabWithFadeAnimation(FloatingActionButton fab) {
// 创建一个从 1 到 0 的值动画
ValueAnimator fadeAnimator = ValueAnimator.ofFloat(1f, 0f);
fadeAnimator.setDuration(300); // 动画持续时间
fadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 获取当前动画值
float value = (float) animation.getAnimatedValue();
// 设置透明度
fab.setAlpha(value);
}
});
fadeAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// 动画结束后隐藏按钮
fab.setVisibility(View.GONE);
}
});
fadeAnimator.start(); // 启动动画
}
在 showFabWithFadeAnimation
方法中,创建一个从 0 到 1 的 ValueAnimator
,并在 onAnimationUpdate
方法中更新 FloatingActionButton
的透明度。在 hideFabWithFadeAnimation
方法中,创建一个从 1 到 0 的 ValueAnimator
,并在动画结束后将按钮的可见性设置为 GONE
。
8.2 移动动画
FloatingActionButton
还可以实现移动动画,通过改变其位置来实现。可以使用 ObjectAnimator
来控制按钮的位置变化。以下是实现移动动画的代码示例:
java
// 移动 FloatingActionButton 的动画
private void moveFab(FloatingActionButton fab, int targetX, int targetY) {
// 创建一个 ObjectAnimator 对象,控制按钮的 X 坐标移动
ObjectAnimator animatorX = ObjectAnimator.ofFloat(fab, "x", fab.getX(), targetX);
// 创建一个 ObjectAnimator 对象,控制按钮的 Y 坐标移动
ObjectAnimator animatorY = ObjectAnimator.ofFloat(fab, "y", fab.getY(), targetY);
// 创建一个 AnimatorSet 对象,将两个动画组合在一起
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animatorX, animatorY);
animatorSet.setDuration(300); // 动画持续时间
animatorSet.start(); // 启动动画
}
在 moveFab
方法中,创建两个 ObjectAnimator
对象,分别控制 FloatingActionButton
的 X 坐标和 Y 坐标的移动。然后使用 AnimatorSet
将两个动画组合在一起,并启动动画。
8.3 动画监听与回调
在动画执行过程中,可以添加监听器来监听动画的状态变化,如动画开始、结束、取消等。以下是添加动画监听器的代码示例:
java
// 显示 FloatingActionButton 的缩放动画,并添加监听器
private void showFabWithScaleAnimationWithListener(FloatingActionButton fab) {
// 创建一个从 0 到 1 的值动画
ValueAnimator scaleAnimator = ValueAnimator.ofFloat(0f, 1f);
scaleAnimator.setDuration(300); // 动画持续时间
scaleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 获取当前动画值
float value = (float) animation.getAnimatedValue();
// 设置缩放比例
fab.setScaleX(value);
fab.setScaleY(value);
}
});
scaleAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
// 动画开始时的回调
Log.d("FabAnimation", "Animation started");
}
@Override
public void onAnimationEnd(Animator animation) {
// 动画结束时的回调
Log.d("FabAnimation", "Animation ended");
}
@Override
public void onAnimationCancel(Animator animation) {
// 动画取消时的回调
Log.d("FabAnimation", "Animation cancelled");
}
});
scaleAnimator.start(); // 启动动画
}
在 showFabWithScaleAnimationWithListener
方法中,为 ValueAnimator
添加了一个 AnimatorListenerAdapter
,并实现了 onAnimationStart
、onAnimationEnd
和 onAnimationCancel
方法,分别在动画开始、结束和取消时进行回调。
九、性能优化
9.1 减少不必要的重绘
在 FloatingActionButton
的使用过程中,频繁的重绘会影响性能。可以通过以下方法减少不必要的重绘:
9.1.1 避免频繁修改属性
尽量避免在短时间内频繁修改 FloatingActionButton
的属性,如背景颜色、图标等。因为每次修改属性都可能触发重绘。例如,不要在循环中频繁调用 setBackgroundTintList
或 setImageDrawable
方法。
9.1.2 使用 invalidate
和 postInvalidate
方法
invalidate
方法用于在主线程中请求重绘,postInvalidate
方法用于在子线程中请求重绘。在需要重绘时,根据实际情况选择合适的方法。例如,如果在子线程中更新了 FloatingActionButton
的状态,应该使用 postInvalidate
方法。
java
// 在子线程中更新 FloatingActionButton 的状态,并请求重绘
new Thread(new Runnable() {
@Override
public void run() {
// 更新状态
// ...
// 请求重绘
fab.postInvalidate();
}
}).start();
9.1.3 合理设置 setWillNotDraw
setWillNotDraw
方法用于设置视图是否需要绘制。如果 FloatingActionButton
不需要进行自定义绘制,可以调用 setWillNotDraw(true)
来减少不必要的绘制操作。
java
// 设置 FloatingActionButton 不需要自定义绘制
fab.setWillNotDraw(true);
9.2 优化阴影效果
阴影效果虽然可以增强视觉效果,但也会消耗一定的性能。可以通过以下方法优化阴影效果:
9.2.1 降低阴影高度
阴影高度越高,绘制阴影所需的计算量就越大。可以根据实际情况适当降低 FloatingActionButton
的阴影高度。例如,使用 setElevation
方法设置较低的阴影高度。
java
// 设置较低的阴影高度
fab.setElevation(5);
9.2.2 使用 HardwareLayer
可以将 FloatingActionButton
的 LayerType
设置为 LAYER_TYPE_HARDWARE
,使用硬件加速来绘制阴影,提高绘制性能。
java
// 设置 FloatingActionButton 使用硬件加速绘制
fab.setLayerType(View.LAYER_TYPE_HARDWARE, null);
9.3 内存管理
在使用 FloatingActionButton
时,需要注意内存管理,避免内存泄漏。以下是一些内存管理的建议:
9.3.1 及时释放资源
在 FloatingActionButton
不再使用时,及时释放其占用的资源,如 Drawable
对象、动画对象等。例如,在 Activity
的 onDestroy
方法中,释放相关资源。
java
@Override
protected void onDestroy() {
super.onDestroy();
// 释放 Drawable 对象
if (fab.getDrawable() != null) {
fab.getDrawable().setCallback(null);
}
// 取消动画
if (animator != null && animator.isRunning()) {
animator.cancel();
}
}
9.3.2 避免静态引用
避免在静态变量中引用 FloatingActionButton
或其相关对象,因为静态变量的生命周期与应用程序的生命周期相同,可能会导致内存泄漏。
9.4 性能优化源码分析
9.4.1 invalidate
方法
invalidate
方法用于请求重绘视图。以下是 View
类中 invalidate
方法的简化源码:
java
// 请求重绘视图的方法
public void invalidate() {
if (isHardwareAccelerated()) {
// 如果使用硬件加速,调用 invalidateInternal 方法
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, true, false);
} else {
// 如果不使用硬件加速,调用 invalidateParentCaches 方法
invalidateParentCaches();
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, true, true);
}
}
// 内部的重绘方法
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
if (skipInvalidate()) {
return;
}
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
mPrivateFlags &= ~PFLAG_DRAWN;
}
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
// Damage the entire projection receiver, if necessary.
if (mBackground != null && mBackground.isProjected()) {
final View receiver = getProjectionReceiver();
if (receiver != null) {
receiver.damageInParent();
}
}
}
}
在 invalidate
方法中,根据是否使用硬件加速,调用不同的方法进行重绘。invalidateInternal
方法会更新视图的状态,并将重绘请求传递给父视图。
9.4.2 setLayerType
方法
setLayerType
方法用于设置视图的 LayerType
。以下是 View
类中 setLayerType
方法的简化源码:
java
// 设置视图的 LayerType 的方法
public void setLayerType(int layerType, Paint paint) {
if (layerType == mLayerType) {
return;
}
mLayerType = layerType;
mLayerPaint = paint;
if (layerType == LAYER_TYPE_HARDWARE) {
// 如果设置为硬件加速,进行相关初始化
if (mAttachInfo != null && mAttachInfo.mHardwareRenderer != null) {
mAttachInfo.mHardwareRenderer.createViewLayer(this);
}
} else if (layerType == LAYER_TYPE_SOFTWARE) {
// 如果设置为软件加速,进行相关初始化
if (mLayer != null) {
mLayer.destroy();
mLayer = null;
}
}
invalidate(true);
}
在 setLayerType
方法中,根据传入的 LayerType
进行相应的初始化操作,并调用 invalidate
方法请求重绘。
十、常见问题及解决方案
10.1 水波纹效果不显示
10.1.1 原因分析
- 背景设置问题 :如果
FloatingActionButton
的背景被设置为其他Drawable
,而不是RippleDrawable
,水波纹效果将无法显示。 - 版本兼容性问题 :在某些低版本的 Android 系统中,
RippleDrawable
的支持可能存在问题。
10.1.2 解决方案
- 检查背景设置 :确保
FloatingActionButton
的背景是RippleDrawable
。可以通过代码或 XML 布局文件进行设置。例如,在 XML 布局文件中使用app:backgroundTint
属性来设置背景颜色,系统会自动创建RippleDrawable
。
xml
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backgroundTint="@color/colorAccent"
app:srcCompat="@drawable/ic_add" />
- 检查版本兼容性 :如果在低版本的 Android 系统中出现问题,可以考虑使用第三方库或自定义
Drawable
来实现水波纹效果。
10.2 水波纹扩散范围异常
10.2.1 原因分析
- 遮罩设置问题 :
RippleDrawable
的遮罩Drawable
可能设置不正确,导致水波纹的扩散范围受到限制。 - 布局问题 :
FloatingActionButton
的布局参数可能影响水波纹的扩散范围。例如,如果按钮的大小设置不合理,水波纹可能无法正常扩散。
10.2.2 解决方案
- 检查遮罩设置 :确保
RippleDrawable
的遮罩Drawable
与按钮的大小和形状一致。可以通过代码或 XML 布局文件进行设置。例如,在 XML 布局文件中使用app:rippleColor
属性来设置水波纹颜色,系统会自动处理遮罩。
xml
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backgroundTint="@color/colorAccent"
app:srcCompat="@drawable/ic_add"
app:rippleColor="@color/rippleColor" />
- 检查布局参数 :确保
FloatingActionButton
的布局参数合理,按钮有足够的空间让水波纹扩散。
10.3 阴影效果不显示
10.3.1 原因分析
- 版本兼容性问题:在某些低版本的 Android 系统中,阴影效果的支持可能存在问题。
- 属性设置问题 :
FloatingActionButton
的elevation
属性可能没有正确设置。
10.3.2 解决方案
- 检查版本兼容性 :如果在低版本的 Android 系统中出现问题,可以考虑使用第三方库或自定义
Drawable
来实现阴影效果。 - 检查属性设置 :确保
FloatingActionButton
的elevation
属性正确设置。可以通过代码或 XML 布局文件进行设置。例如,在 XML 布局文件中使用app:elevation
属性来设置阴影高度。
xml
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backgroundTint="@color/colorAccent"
app:srcCompat="@drawable/ic_add"
app:elevation="8dp" />
10.4 点击事件无响应
10.4.1 原因分析
- 事件监听器未设置 :可能没有为
FloatingActionButton
设置点击事件监听器。 - 父视图拦截事件 :父视图可能拦截了点击事件,导致
FloatingActionButton
无法接收到事件。
10.4.2 解决方案
- 检查事件监听器设置 :确保为
FloatingActionButton
设置了点击事件监听器。可以通过代码进行设置。例如:
java
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 处理点击事件
Toast.makeText(MainActivity.this, "FAB clicked!", Toast.LENGTH_SHORT).show();
}
});
- 检查父视图事件拦截 :检查父视图是否拦截了点击事件。可以通过重写父视图的
onInterceptTouchEvent
方法来控制事件的拦截。例如:
java
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 不拦截事件,让子视图处理
return false;
}
十一、总结与展望
11.1 总结
通过对 Android FloatingActionButton 的深入分析,我们全面了解了其使用原理和内部实现机制。FloatingActionButton 作为 Material Design 设计语言中的重要组件,以其独特的悬浮式设计和丰富的交互效果,为 Android 应用的用户界面增添了现代感和便捷性。
从基础的 XML 资源定义和属性设置,到复杂的测量、布局、绘制过程,再到点击事件处理和动画效果实现,FloatingActionButton 的每一个环节都经过精心设计和优化。它继承自 ImageButton
类,具备 ImageView
的基本特性,同时融入了 Material Design 的风格和动画效果。通过 RippleDrawable
实现水波纹点击效果,通过 ValueAnimator
和 ObjectAnimator
实现各种动画效果,通过与 CoordinatorLayout
的配合实现与其他视图的交互。
在性能优化方面,我们可以通过减少不必要的重绘、优化阴影效果和合理进行内存管理等方法,提高 FloatingActionButton 的性能和响应速度。同时,我们也分析了常见问题及解决方案,帮助开发者在实际使用中避免和解决各种问题。
11.2 展望
随着 Android 技术的不断发展,FloatingActionButton 可能会在以下几个方面得到进一步的改进和扩展:
11.2.1 更多的动画效果和交互方式
未来,FloatingActionButton 可能会支持更多种类的动画效果和交互方式。例如,除了现有的缩放、淡入淡出和移动动画,还可能会增加旋转、抖动等动画效果。在交互方面,可能会支持更多的手势操作,如长按拖动、双指缩放等,为用户带来更加丰富和有趣的交互体验。
11.2.2 更好的兼容性和定制性
随着 Android 系统版本的不断更新和设备的多样化,FloatingActionButton 需要具备更好的兼容性。同时,开发者对组件的定制性需求也越来越高。未来,FloatingActionButton 可能会提供更多的定制选项,如自定义形状、自定义动画曲线等,让开发者能够根据自己的需求进行个性化定制。
11.2.3 与其他组件的深度集成
FloatingActionButton 可能会与其他 Android 组件进行更深度的集成,实现更加复杂和强大的功能。例如,与 NavigationView
集成,实现导航菜单的快速切换;与 SearchView
集成,实现快速搜索功能等。通过与其他组件的协同工作,FloatingActionButton 能够为用户提供更加高效和便捷的操作体验。
11.2.4 性能的进一步优化
随着设备性能的提升和用户对应用流畅度要求的提高,FloatingActionButton 的性能优化仍然是一个重要的方向。未来,可能会采用更高效的绘制算法、减少内存占用等方法,进一步提高 FloatingActionButton 的性能,确保在各种设备上都能提供流畅的交互体验。
总之,Android FloatingActionButton 作为一个优秀的 UI 组件,在未来的 Android 开发中仍然具有广阔的发展前景。开发者可以充分利用其特性和优势,创造出更加出色的 Android 应用。
以上博客从多个维度深入剖析了 Android FloatingActionButton 的使用原理,涵盖了其构造、属性、测量布局、绘制、事件处理、动画实现等方面,并且对性能优化、常见问题及解决方案进行了详细阐述,最后对其未来发展进行了展望。希望能满足你的需求,若你还有其他修改意见,比如对某些部分进行拓展、精简等,随时告诉我。