android-process-button,扁平化带进度显示的按钮,来自Google的开发专家dmytrodanylyk的作品。
显示效果如图所示。
项目地址:
https://github.com/dmytrodanylyk/android-process-button
二、源码分析
(1).类的概要
1. FlatButton,继承自Button。处理按钮的基本样式,包括背景,圆角和文字。
2. ProcessButton,继承自FlatButton。处理进度相关的显示,包括进度条样式、与进度相关的文字,并提供drawProgress()抽象方法由子类去实现。
3.剩下的三个类, ActionProcessButton, SubmitProcessButton, GenerateProcessButton,都继承自ProcessButton。各自实现了drawProgress()方法,绘制不同样式的进度条。
ProcessButton是一个抽象类,继承自FlatButton。FlatButton已经完成了按钮的基本样式显示,ProcessButton就只专心负责进度条了。从类的定义和成员变量很明显可以看出来。
这里有两个核心方法需要说明。一个是onDraw()方法,对onDraw()重写,在方法内部实现进度条的绘制。但是ProcessButton是一个抽象类,并不知道按钮的具体显示样式,所以提供了抽象方法drawProgress()由子类去实现。
基础类FlatButton和ProcessButton已介绍完毕,有了这两个类,按钮已经具备显示各种样式,处理进度的功能了。唯一欠缺的,就是进度条以何种样式来显示。下面进入三个具体的实现类,每个类会根据自身的特点来重写drawProgress()方法。
(5).类的详解之具体实现类SubmitProcessButton
先上效果图。
(6).类的详解之具体实现类GenerateProcessButton
先上效果图。
显示效果如图所示。
项目地址:
https://github.com/dmytrodanylyk/android-process-button
其中包含项目源码和Demo。
一、项目使用
(1).项目的引用
dependencies {
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.0'
}
(2).自定义属性的介绍
<resources>
<declare-styleable name="ProcessButton">
<!-- “加载中”显示的文字 -->
<attr name="pb_textProgress" format="string" />
<!-- “加载完成”显示的文字 -->
<attr name="pb_textComplete" format="string" />
<!-- “加载出错”显示的文字 -->
<attr name="pb_textError" format="string" />
<!-- “加载中”进度条背景 -->
<attr name="pb_colorProgress" format="color" />
<!-- “加载完成”进度条背景 -->
<attr name="pb_colorComplete" format="color" />
<!-- “加载出错”进度条背景 -->
<attr name="pb_colorError" format="color" />
</declare-styleable>
<declare-styleable name="FlatButton">
<!-- 按钮按下后的背景色 -->
<attr name="pb_colorPressed" format="color" />
<!-- 按钮默认的背景色 -->
<attr name="pb_colorNormal" format="color" />
<!-- 按钮的圆角半径 -->
<attr name="pb_cornerRadius" format="dimension" />
</declare-styleable>
</resources>
二、源码分析
(1).类的概要
1. FlatButton,继承自Button。处理按钮的基本样式,包括背景,圆角和文字。
2. ProcessButton,继承自FlatButton。处理进度相关的显示,包括进度条样式、与进度相关的文字,并提供drawProgress()抽象方法由子类去实现。
3.剩下的三个类, ActionProcessButton, SubmitProcessButton, GenerateProcessButton,都继承自ProcessButton。各自实现了drawProgress()方法,绘制不同样式的进度条。
类的继承图:
(2).类的详解之基础类FlatButton
先来分析最基础的类FlatButton,该类较简单,主要是在构造器内调用init()方法进行初始化。
从类的定义可以看出,它继承自系统的Button类。
public class FlatButton extends Button {
}
成员变量,定义了背景、文字和圆角半径。
// 背景
private StateListDrawable mNormalDrawable;
// 文字
private CharSequence mNormalText;
// 圆角半径
private float cornerRadius;
三个成员变量都支持自定义设置。在项目内部提供了rect_normal.xml文件,作为按钮的默认显示背景。rect_normal.xml是一个layer-list类型drawable文件,内部包含2个item。然后在java代码中将其加载为LayerDrawable对象,取出每一个item,再根据用户设置的颜色和圆角对其进行修改。成员变量mNormalDrawable是StateListDrawable类型对象,这里采用动态代码添加各种状态,包括state_pressed、state_focused、state_selected和默认状态。
// 初始化cornerRadius和mNormalDrawable
private void initAttributes(Context context, AttributeSet attributeSet) {
TypedArray attr = getTypedArray(context, attributeSet, R.styleable.FlatButton);
if (attr == null) {
return;
}
try {
float defValue = getDimension(R.dimen.corner_radius);
// 可以通过自定义属性pb_cornerRadius修改圆角半径
cornerRadius = attr.getDimension(R.styleable.FlatButton_pb_cornerRadius, defValue);
// mNormalDrawable是StateListDrawable类型对象,这里采用动态代码添加各种状态,包括state_pressed、state_focused、state_selected和默认
mNormalDrawable.addState(new int[]{android.R.attr.state_pressed}, createPressedDrawable(attr));
mNormalDrawable.addState(new int[]{android.R.attr.state_focused}, createPressedDrawable(attr));
mNormalDrawable.addState(new int[]{android.R.attr.state_selected}, createPressedDrawable(attr));
mNormalDrawable.addState(new int[]{}, createNormalDrawable(attr));
} finally {
attr.recycle();
}
}
// 创建默认的按钮背景
private LayerDrawable createNormalDrawable(TypedArray attr) {
// rect_normal是一个layer-list类型drawable文件,内部包含2个item
LayerDrawable drawableNormal = (LayerDrawable) getDrawable(R.drawable.rect_normal).mutate();
// 可以通过修改cornerRadius成员变量来改变圆角半径
GradientDrawable drawableTop = (GradientDrawable) drawableNormal.getDrawable(0).mutate();
drawableTop.setCornerRadius(getCornerRadius());
// 按钮按下之后的颜色,默认深蓝色,可通过pb_colorPressed自定义属性修改
int blueDark = getColor(R.color.blue_pressed);
int colorPressed = attr.getColor(R.styleable.FlatButton_pb_colorPressed, blueDark);
drawableTop.setColor(colorPressed);
GradientDrawable drawableBottom = (GradientDrawable) drawableNormal.getDrawable(1).mutate();
drawableBottom.setCornerRadius(getCornerRadius());
// 按钮默认的颜色,默认浅蓝色,可通过pb_colorNormal自定义属性修改
int blueNormal = getColor(R.color.blue_normal);
int colorNormal = attr.getColor(R.styleable.FlatButton_pb_colorNormal, blueNormal);
drawableBottom.setColor(colorNormal);
return drawableNormal;
}
// 创建获取到焦点后的按钮背景
private Drawable createPressedDrawable(TypedArray attr) {
GradientDrawable drawablePressed = (GradientDrawable) getDrawable(R.drawable.rect_pressed).mutate();
drawablePressed.setCornerRadius(getCornerRadius());
int blueDark = getColor(R.color.blue_pressed);
int colorPressed = attr.getColor(R.styleable.FlatButton_pb_colorPressed, blueDark);
drawablePressed.setColor(colorPressed);
return drawablePressed;
}
使用到的rect_normal.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/corner_radius" />
<solid android:color="@color/blue_pressed" />
</shape>
</item>
<item android:bottom="@dimen/layer_padding">
<shape android:shape="rectangle">
<corners android:radius="@dimen/corner_radius" />
<solid android:color="@color/blue_normal" />
</shape>
</item>
</layer-list>
有了Drawable类型对象后,需要设置为按钮背景。在调用Button类的setBackground方法时,这里有两个小细节。一,兼容性,Android在4.1之前使用的是setBackgroundDrawable()方法,4.1之后才有了setBackground()方法;二,如果为按钮添加了内间距padding需要注意,由于Button在调用setBackgroundXXX(drawable)方法时,会使用参数drawable的padding值,Button的padding值会失效,所以需要先将padding值保存起来,在setBackgroundXXX()方法被调用之后,再设置Button的padding。
// 设置按钮的背景
// 分两步:(1).setBackground(),(2).setPadding()
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
public void setBackgroundCompat(Drawable drawable) {
// 因为在调用setBackgroundXXX()方法时,会使用参数drawable的padding值,原View的padding值失效
// 所以这里先将padding值保存起来,在setBackgroundXXX()方法被调用之后,再设置View的padding
int pL = getPaddingLeft();
int pT = getPaddingTop();
int pR = getPaddingRight();
int pB = getPaddingBottom();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
setBackground(drawable);
} else {
setBackgroundDrawable(drawable);
}
setPadding(pL, pT, pR, pB);
}
准备工作完毕,接下来需要在构造方法中执行了。
public FlatButton(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// 创建mNormalDrawable
mNormalDrawable = new StateListDrawable();
if (attrs != null) {
initAttributes(context, attrs);
}
// 取按钮中的文字
mNormalText = getText().toString();
// 设置mNormalDrawable为按钮背景
setBackgroundCompat(mNormalDrawable);
}
(3).类的详解之基础类ProcessButton
ProcessButton是一个抽象类,继承自FlatButton。FlatButton已经完成了按钮的基本样式显示,ProcessButton就只专心负责进度条了。从类的定义和成员变量很明显可以看出来。
public abstract class ProcessButton extends FlatButton {
// 当前进度
private int mProgress;
// 最大进度
private int mMaxProgress;
// 最小进度
private int mMinProgress;
// “加载中”进度条背景
private GradientDrawable mProgressDrawable;
// “加载完成”进度条背景
private GradientDrawable mCompleteDrawable;
// “加载出错”进度条背景
private GradientDrawable mErrorDrawable;
// “加载中”显示的文字
private CharSequence mLoadingText;
// “加载完成”显示的文字
private CharSequence mCompleteText;
// “加载出错”显示的文字
private CharSequence mErrorText;
}
接下来成员变量在构造方法中完成初始化,支持用户自定义设置属性,当没有设置时会取默认值。这些与FlatButton如出一辙,不再累述。
这里有两个核心方法需要说明。一个是onDraw()方法,对onDraw()重写,在方法内部实现进度条的绘制。但是ProcessButton是一个抽象类,并不知道按钮的具体显示样式,所以提供了抽象方法drawProgress()由子类去实现。
@Override
protected void onDraw(Canvas canvas) {
// 绘制自定义进度条
if (mProgress > mMinProgress && mProgress < mMaxProgress) {
drawProgress(canvas);
}
// super.onDraw()一定要调用,绘制按钮自身
super.onDraw(canvas);
}
public abstract void drawProgress(Canvas canvas);
另一个方法是setProgress(int progress),根据传入的参数progress来更新按钮的显示,包括进度条的样式和文字。
// 根据当前进度,刷新按钮的进度条和文字,最后一定要调用invalidate()
public void setProgress(int progress) {
mProgress = progress;
if (mProgress == mMinProgress) {
onNormalState();
} else if (mProgress == mMaxProgress) {
onCompleteState();
} else if (mProgress < mMinProgress) {
onErrorState();
} else {
onProgress();
}
invalidate();
}
// 显示为:出错了
protected void onErrorState() {
if (getErrorText() != null) {
setText(getErrorText());
}
setBackgroundCompat(getErrorDrawable());
}
// 显示为:加载中
protected void onProgress() {
if (getLoadingText() != null) {
setText(getLoadingText());
}
setBackgroundCompat(getNormalDrawable());
}
// 显示为:执行完毕
protected void onCompleteState() {
if (getCompleteText() != null) {
setText(getCompleteText());
}
setBackgroundCompat(getCompleteDrawable());
}
// 显示为:默认状态
protected void onNormalState() {
if (getNormalText() != null) {
setText(getNormalText());
}
setBackgroundCompat(getNormalDrawable());
}
基础类FlatButton和ProcessButton已介绍完毕,有了这两个类,按钮已经具备显示各种样式,处理进度的功能了。唯一欠缺的,就是进度条以何种样式来显示。下面进入三个具体的实现类,每个类会根据自身的特点来重写drawProgress()方法。
(4).类的详解之具体实现类ActionProcessButton
本文开篇的效果图就是ActionProcessButton类实现的。来看看drawProgress()方法是如何实现的。
// 将进度条绘制到按钮底部
@Override
public void drawProgress(Canvas canvas) {
// 当前进度条的比例
float scale = (float) getProgress() / (float) getMaxProgress();
// indicatorWidth:绘制区域的右侧,随着进度的进行,该值不断扩大直到按钮的宽度
float indicatorWidth = (float) getMeasuredWidth() * scale;
// bottom:绘制区域的顶部
double indicatorHeightPercent = 0.05;
int bottom = (int) (getMeasuredHeight() - getMeasuredHeight() * indicatorHeightPercent);
// setBounds()指定绘制的区域
getProgressDrawable().setBounds(0, bottom, (int) indicatorWidth, getMeasuredHeight());
// 最后要调用draw()方法绘制
getProgressDrawable().draw(canvas);
}
这里将进度条绘制到按钮底部,绘制区域中的left,top,bottom都是保持不变的,只有right随着progress值的增加不断的变大,直到达到按钮的宽度,进度执行完毕。
(5).类的详解之具体实现类SubmitProcessButton
先上效果图。
该类代码很少,除了构造方法外,只有drawProgress()方法的实现。
// 进度条填充按钮高度,只有宽度在不断的增加
@Override
public void drawProgress(Canvas canvas) {
float scale = (float) getProgress() / (float) getMaxProgress();
float indicatorWidth = (float) getMeasuredWidth() * scale;
getProgressDrawable().setBounds(0, 0, (int) indicatorWidth, getMeasuredHeight());
getProgressDrawable().draw(canvas);
}
实现方式同上。依旧是调用Drawable的setBounds()方法,修改绘制区域。保持高度不变,不断的增加宽度。
(6).类的详解之具体实现类GenerateProcessButton
先上效果图。
该类的实现几乎与SubmitProcessButton一致,唯一的区别是,在进度显示上反过来了,保持宽度不变,不断的刷新高度。
// 进度条填充按钮宽度,只有高度在不断的增加
@Override
public void drawProgress(Canvas canvas) {
float scale = (float) getProgress() / (float) getMaxProgress();
float indicatorHeight = (float) getMeasuredHeight() * scale;
getProgressDrawable().setBounds(0, 0, getMeasuredWidth(), (int) indicatorHeight);
getProgressDrawable().draw(canvas);
}