編輯:關於Android編程
今天再來介紹該作者的另一個開源項目circular-progress-button,效果更酷炫。
項目地址:
https://github.com/dmytrodanylyk/circular-progress-button
其中包含項目源碼和示例代碼。
運行效果圖:

在分析該項目的源碼之前,需要一些准備工作。關於Drawable,需要熟悉GradientDrawable和StateListDrawable類,ColorStateList類,以及如何繼承Drawable類實現自己的drawable。對於動畫,需要了解ValueAnimator和ObjectAnimator類的使用。
一、核心類的介紹
CircularProgressButton:圓形進度按鈕,引用該開源項目時使用的控件。
MorphingAnimation:執行按鈕的變換動畫。比如從按鈕變成圓環,按鈕在不同狀態之間的變換。
CircularProgressDrawable:圓環進度的Drawable,進度從0執行到100即結束。
CircularAnimatedDrawable:圓環動畫的Drawable,使用該Drawable圓環會一直循環執行動畫。
二、初始化
(1).成員變量介紹
private StrokeGradientDrawable background;// 背景
private CircularAnimatedDrawable mAnimatedDrawable;// 圓環動畫
private CircularProgressDrawable mProgressDrawable;// 圓環進度
private ColorStateList mIdleColorState;// 默認
private ColorStateList mCompleteColorState;// 完成
private ColorStateList mErrorColorState;// 錯誤
private StateListDrawable mIdleStateDrawable;// 默認
private StateListDrawable mCompleteStateDrawable;// 完成
private StateListDrawable mErrorStateDrawable;// 錯誤
private String mIdleText;// 默認
private String mCompleteText;// 完成
private String mErrorText;// 錯誤
private String mProgressText;// 進度中
private enum State {
IDLE,// 默認
PROGRESS,// 進度中
COMPLETE,// 完成
ERROR// 錯誤
}
// 初始化mErrorStateDrawable
private void initErrorStateDrawable() {
int colorPressed = getPressedColor(mErrorColorState);
StrokeGradientDrawable drawablePressed = createDrawable(colorPressed);
mErrorStateDrawable = new StateListDrawable();
mErrorStateDrawable.addState(new int[]{android.R.attr.state_pressed}, drawablePressed.getGradientDrawable());
mErrorStateDrawable.addState(StateSet.WILD_CARD, background.getGradientDrawable());
}
// 初始化mCompleteStateDrawable
private void initCompleteStateDrawable() {
int colorPressed = getPressedColor(mCompleteColorState);
StrokeGradientDrawable drawablePressed = createDrawable(colorPressed);
mCompleteStateDrawable = new StateListDrawable();
mCompleteStateDrawable.addState(new int[]{android.R.attr.state_pressed}, drawablePressed.getGradientDrawable());
mCompleteStateDrawable.addState(StateSet.WILD_CARD, background.getGradientDrawable());
}
// 初始化mIdleStateDrawable
private void initIdleStateDrawable() {
int colorNormal = getNormalColor(mIdleColorState);
int colorPressed = getPressedColor(mIdleColorState);
int colorFocused = getFocusedColor(mIdleColorState);
int colorDisabled = getDisabledColor(mIdleColorState);
if (background == null) {
background = createDrawable(colorNormal);
}
StrokeGradientDrawable drawableDisabled = createDrawable(colorDisabled);
StrokeGradientDrawable drawableFocused = createDrawable(colorFocused);
StrokeGradientDrawable drawablePressed = createDrawable(colorPressed);
mIdleStateDrawable = new StateListDrawable();
mIdleStateDrawable.addState(new int[]{android.R.attr.state_pressed}, drawablePressed.getGradientDrawable());
mIdleStateDrawable.addState(new int[]{android.R.attr.state_focused}, drawableFocused.getGradientDrawable());
mIdleStateDrawable.addState(new int[]{-android.R.attr.state_enabled}, drawableDisabled.getGradientDrawable());
mIdleStateDrawable.addState(StateSet.WILD_CARD, background.getGradientDrawable());
}
private int getNormalColor(ColorStateList colorStateList) {
return colorStateList.getColorForState(new int[]{android.R.attr.state_enabled}, 0);
}
private int getPressedColor(ColorStateList colorStateList) {
return colorStateList.getColorForState(new int[]{android.R.attr.state_pressed}, 0);
}
private int getFocusedColor(ColorStateList colorStateList) {
return colorStateList.getColorForState(new int[]{android.R.attr.state_focused}, 0);
}
private int getDisabledColor(ColorStateList colorStateList) {
return colorStateList.getColorForState(new int[]{-android.R.attr.state_enabled}, 0);
}
關於顯示文字、圖標,進度條顏色值的初始化不再說明,很好理解。
三、執行進度變化
(1).setProgress(int progress)方法
根據當前進度值改變按鈕的顯示狀態,需要調用setProgress(int progress)方法。
// 改變進度
public void setProgress(int progress) {
mProgress = progress;
if (mMorphingInProgress || getWidth() == 0) {
return;
}
mStateManager.saveProgress(this);
// 判斷進度mProgress和狀態mState
// mProgress是當前方法參數傳遞進來的;mState是在每一個動畫執行結束後被賦值的
if (mProgress >= mMaxProgress) {// 當前進度大於等於最大值
if (mState == State.PROGRESS) {
morphProgressToComplete();// 加載中 --> 完成
} else if (mState == State.IDLE) {
morphIdleToComplete();// 初始 --> 完成
}
} else if (mProgress > IDLE_STATE_PROGRESS) {// 當前進度大於初始值,小於最大值
if (mState == State.IDLE) {
morphToProgress();// 初始 --> 加載中
} else if (mState == State.PROGRESS) {
invalidate();// 直接繪制
}
} else if (mProgress == ERROR_STATE_PROGRESS) {// 當前進度等於錯誤值
if (mState == State.PROGRESS) {
morphProgressToError();// 加載中 --> 錯誤
} else if (mState == State.IDLE) {
morphIdleToError();// 初始 --> 錯誤
}
} else if (mProgress == IDLE_STATE_PROGRESS) {// 當前進度等於初始值
if (mState == State.COMPLETE) {
morphCompleteToIdle();// 完成 --> 初始
} else if (mState == State.PROGRESS) {
morphProgressToIdle();// 加載中 --> 初始
} else if (mState == State.ERROR) {
morphErrorToIdle();// 錯誤 --> 初始
}
}
}
在方法內部,對傳入的progress值和按鈕當前的狀態mState進行判斷,調用相應的morphXXToXX()方法,來改變按鈕的顯示效果。
(2).morph()方法
setProgress(int progress)方法的核心,是調用各種不同的morph方法。
// 按鈕的不同狀態之間變換,使用該動畫
private MorphingAnimation createMorphing() {
mMorphingInProgress = true;
MorphingAnimation animation = new MorphingAnimation(this, background);
animation.setFromCornerRadius(mCornerRadius);
animation.setToCornerRadius(mCornerRadius);
animation.setFromWidth(getWidth());
animation.setToWidth(getWidth());
return animation;
}
// 按鈕與圓環之間變換,使用該動畫
// 相對於createMorphing()方法,增加了圓角和寬度的變化
private MorphingAnimation createProgressMorphing(float fromCorner, float toCorner, int fromWidth, int toWidth) {
mMorphingInProgress = true;
MorphingAnimation animation = new MorphingAnimation(this, background);
animation.setFromCornerRadius(fromCorner);
animation.setToCornerRadius(toCorner);
animation.setPadding(mPaddingProgress);
animation.setFromWidth(fromWidth);
animation.setToWidth(toWidth);
return animation;
}
// 初始 --> 加載中,由按鈕變成圓環
private void morphToProgress() {
setWidth(getWidth());
setText(mProgressText);
// CornerRadius變化:按鈕的圓角mCornerRadius -> 圓環的高度getHeight()
// Width變化:按鈕的寬度getWidth() -> 圓環的寬度getHeight()
MorphingAnimation animation = createProgressMorphing(mCornerRadius, getHeight(), getWidth(), getHeight());
animation.setFromColor(getNormalColor(mIdleColorState));
animation.setToColor(mColorProgress);
animation.setFromStrokeColor(getNormalColor(mIdleColorState));
animation.setToStrokeColor(mColorIndicatorBackground);
animation.setListener(mProgressStateListener);
animation.start();
}
// 加載中 --> 完成
private void morphProgressToComplete() {
// CornerRadius變化:圓角的高度getHeight() -> 按鈕的圓角mCornerRadius
// Width變化:圓環的寬度getHeight() -> 按鈕的寬度getWidth()
MorphingAnimation animation = createProgressMorphing(getHeight(), mCornerRadius, getHeight(), getWidth());
animation.setFromColor(mColorProgress);
animation.setToColor(getNormalColor(mCompleteColorState));
animation.setFromStrokeColor(mColorIndicator);
animation.setToStrokeColor(getNormalColor(mCompleteColorState));
animation.setListener(mCompleteStateListener);
animation.start();
}
// 加載中 --> 錯誤
private void morphProgressToError() {
MorphingAnimation animation = createProgressMorphing(getHeight(), mCornerRadius, getHeight(), getWidth());
animation.setFromColor(mColorProgress);
animation.setToColor(getNormalColor(mErrorColorState));
animation.setFromStrokeColor(mColorIndicator);
animation.setToStrokeColor(getNormalColor(mErrorColorState));
animation.setListener(mErrorStateListener);
animation.start();
}
// 加載中 --> 初始
private void morphProgressToIdle() {
// CornerRadius變化:圓角的高度getHeight() -> 按鈕的圓角mCornerRadius
// Width變化:圓環的寬度getHeight() -> 按鈕的寬度getWidth()
MorphingAnimation animation = createProgressMorphing(getHeight(), mCornerRadius, getHeight(), getWidth());
animation.setFromColor(mColorProgress);
animation.setToColor(getNormalColor(mIdleColorState));
animation.setFromStrokeColor(mColorIndicator);
animation.setToStrokeColor(getNormalColor(mIdleColorState));
animation.setListener(new OnAnimationEndListener() {
@Override
public void onAnimationEnd() {
removeIcon();
setText(mIdleText);
mMorphingInProgress = false;
mState = State.IDLE;
mStateManager.checkState(CircularProgressButton.this);
}
});
animation.start();
}
// 初始 --> 完成
private void morphIdleToComplete() {
MorphingAnimation animation = createMorphing();
animation.setFromColor(getNormalColor(mIdleColorState));
animation.setToColor(getNormalColor(mCompleteColorState));
animation.setFromStrokeColor(getNormalColor(mIdleColorState));
animation.setToStrokeColor(getNormalColor(mCompleteColorState));
animation.setListener(mCompleteStateListener);
animation.start();
}
// 完成 --> 初始
private void morphCompleteToIdle() {
MorphingAnimation animation = createMorphing();
animation.setFromColor(getNormalColor(mCompleteColorState));
animation.setToColor(getNormalColor(mIdleColorState));
animation.setFromStrokeColor(getNormalColor(mCompleteColorState));
animation.setToStrokeColor(getNormalColor(mIdleColorState));
animation.setListener(mIdleStateListener);
animation.start();
}
// 錯誤 --> 初始
private void morphErrorToIdle() {
MorphingAnimation animation = createMorphing();
animation.setFromColor(getNormalColor(mErrorColorState));
animation.setToColor(getNormalColor(mIdleColorState));
animation.setFromStrokeColor(getNormalColor(mErrorColorState));
animation.setToStrokeColor(getNormalColor(mIdleColorState));
animation.setListener(mIdleStateListener);
animation.start();
}
// 初始 --> 錯誤
private void morphIdleToError() {
MorphingAnimation animation = createMorphing();
animation.setFromColor(getNormalColor(mIdleColorState));
animation.setToColor(getNormalColor(mErrorColorState));
animation.setFromStrokeColor(getNormalColor(mIdleColorState));
animation.setToStrokeColor(getNormalColor(mErrorColorState));
animation.setListener(mErrorStateListener);
animation.start();
}
(3).MorphingAnimation類。
public void start() {
// 大小變化的動畫
ValueAnimator widthAnimation = ValueAnimator.ofInt(mFromWidth, mToWidth);
final GradientDrawable gradientDrawable = mDrawable.getGradientDrawable();
widthAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Integer value = (Integer) animation.getAnimatedValue();
int leftOffset;
int rightOffset;
int padding;
if (mFromWidth > mToWidth) {// 從按鈕變成圓形進度
leftOffset = (mFromWidth - value) / 2;
rightOffset = mFromWidth - leftOffset;
padding = (int) (mPadding * animation.getAnimatedFraction());
} else {// 從圓形進度變成按鈕
leftOffset = (mToWidth - value) / 2;
rightOffset = mToWidth - leftOffset;
padding = (int) (mPadding - mPadding * animation.getAnimatedFraction());
}
gradientDrawable
.setBounds(leftOffset + padding, padding, rightOffset - padding, mView.getHeight() - padding);
}
});
// 背景色變化的動畫
ObjectAnimator bgColorAnimation = ObjectAnimator.ofInt(gradientDrawable, "color", mFromColor, mToColor);
bgColorAnimation.setEvaluator(new ArgbEvaluator());
// 描邊色變化的動畫
ObjectAnimator strokeColorAnimation =
ObjectAnimator.ofInt(mDrawable, "strokeColor", mFromStrokeColor, mToStrokeColor);
strokeColorAnimation.setEvaluator(new ArgbEvaluator());
// 圓角變化的動畫
ObjectAnimator cornerAnimation =
ObjectAnimator.ofFloat(gradientDrawable, "cornerRadius", mFromCornerRadius, mToCornerRadius);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(mDuration);
animatorSet.playTogether(widthAnimation, bgColorAnimation, strokeColorAnimation, cornerAnimation);
animatorSet.start();
}
(4).onDraw(Canvas canvas)方法
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 狀態為加載中時,調用drawXXX()繪制圓環
if (mProgress > 0 && mState == State.PROGRESS && !mMorphingInProgress) {
if (mIndeterminateProgressMode) {
drawIndeterminateProgress(canvas);
} else {
drawProgress(canvas);
}
}
}
// 繪制循環進度的圓環
private void drawIndeterminateProgress(Canvas canvas) {
if (mAnimatedDrawable == null) {
int offset = (getWidth() - getHeight()) / 2;
mAnimatedDrawable = new CircularAnimatedDrawable(mColorIndicator, mStrokeWidth);
// 左間距:按鈕寬度的一半 - 圓環的半徑
int left = offset + mPaddingProgress;
// 右間距:按鈕的寬度 - 圓環距離按鈕右側的距離
int right = getWidth() - offset - mPaddingProgress;
// 下間距:圓環的直徑
int bottom = getHeight() - mPaddingProgress;
// 上間距
int top = mPaddingProgress;
mAnimatedDrawable.setBounds(left, top, right, bottom);
mAnimatedDrawable.setCallback(this);
mAnimatedDrawable.start();
} else {
mAnimatedDrawable.draw(canvas);
}
}
// 根據進度值繪制圓環
private void drawProgress(Canvas canvas) {
if (mProgressDrawable == null) {
// offset:計算圓環左側距離按鈕的間距,這裡的getHeight()代表圓環的直徑
int offset = (getWidth() - getHeight()) / 2;
// size:圓環的直徑
int size = getHeight() - mPaddingProgress * 2;
// 初始化CircularProgressDrawable,傳入參數直徑、描邊寬度、描邊顏色
mProgressDrawable = new CircularProgressDrawable(size, mStrokeWidth, mColorIndicator);
// 如果有padding值,再把padding加上。mPaddingProgress默認為0,指的是圓環和按鈕之間的間距,大於0時圓環會變小
int left = offset + mPaddingProgress;
// 設置Bounds,在CircularProgressDrawable類中會用到Bounds的left和top
mProgressDrawable.setBounds(left, mPaddingProgress, left, mPaddingProgress);
}
// 計算進度條的弧度,最多360°,使用當前進度的比例*360
float sweepAngle = ((float) mProgress / mMaxProgress) * 360;
mProgressDrawable.setSweepAngle(sweepAngle);
mProgressDrawable.draw(canvas);
}
(5).CircularProgressDrawable類
@Override
public void draw(Canvas canvas) {
final Rect bounds = getBounds();
if (mPath == null) {
mPath = new Path();
}
mPath.reset();
// 畫圓弧,mSweepAngle:精度條圓弧,0~360
mPath.addArc(getRect(), mStartAngle, mSweepAngle);
// 移動到按鈕中間
mPath.offset(bounds.left, bounds.top);
// 繪制
canvas.drawPath(mPath, createPaint());
}
private RectF getRect() {
if (mRectF == null) {
int index = mStrokeWidth / 2;
mRectF = new RectF(index, index, getSize() - index, getSize() - index);
}
return mRectF;
}
(6).CircularAnimatedDrawable類
mObjectAnimatorAngle和mObjectAnimatorSweep的初始化如下。
// 初始化動畫,兩個動畫執行時都是在改變某個成員變量的值,然後在onDraw()方法中使用
private void setupAnimations() {
// mObjectAnimatorAngle:圓弧整體勻速旋轉的動畫
// mAngleProperty為成員變量mCurrentGlobalAngle賦值,范圍0~360。mCurrentGlobalAngle勻速增加
mObjectAnimatorAngle = ObjectAnimator.ofFloat(this, mAngleProperty, 360f);
// 勻速
mObjectAnimatorAngle.setInterpolator(ANGLE_INTERPOLATOR);
mObjectAnimatorAngle.setDuration(ANGLE_ANIMATOR_DURATION);
// 循環執行
mObjectAnimatorAngle.setRepeatMode(ValueAnimator.RESTART);
mObjectAnimatorAngle.setRepeatCount(ValueAnimator.INFINITE);
// mObjectAnimatorSweep:圓弧的弧度變化的動畫
// mSweepProperty為成員變量mCurrentSweepAngle賦值,范圍0~300。mCurrentSweepAngle減速增加
mObjectAnimatorSweep = ObjectAnimator.ofFloat(this, mSweepProperty, 360f - MIN_SWEEP_ANGLE * 2);
// 減速
mObjectAnimatorSweep.setInterpolator(SWEEP_INTERPOLATOR);
mObjectAnimatorSweep.setDuration(SWEEP_ANIMATOR_DURATION);
// 循環執行
mObjectAnimatorSweep.setRepeatMode(ValueAnimator.RESTART);
mObjectAnimatorSweep.setRepeatCount(ValueAnimator.INFINITE);
mObjectAnimatorSweep.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
// 每輪動畫結束時,調用該方法
toggleAppearingMode();
}
});
}
// 切換顯示模式
private void toggleAppearingMode() {
// mModeAppearing值取反
mModeAppearing = !mModeAppearing;
if (mModeAppearing) {
// mCurrentGlobalAngleOffset從0開始,每次增加60,直到最大值300,再從頭開始。循環時每間隔一輪變一次
mCurrentGlobalAngleOffset = (mCurrentGlobalAngleOffset + MIN_SWEEP_ANGLE * 2) % 360;
}
}
mObjectAnimatorAngle動畫比較好理解。在執行時,mCurrentGlobalAngle每輪都是從0°遞增到360°,而mCurrentGlobalAngle改變的是drawArc()中startAngle參數的值,從而達到視覺上圓弧勻速旋轉的動畫。如果只分解出mCurrentGlobalAngle動畫產生的繪制,onDraw()相當於如下代碼。
@Override
public void draw(Canvas canvas) {
canvas.drawArc(fBounds, mCurrentGlobalAngle, 330, false, mPaint);
}
難點在於mObjectAnimatorSweep動畫。圓弧在變化時,有兩個極端狀態,最大弧度(弧度為330°,顯示效果上起點和終點間隔30°的灰色)和最小弧度(弧度為30°,顯示效果上起點和終點之間為30°的藍色)。
@Override
public void draw(Canvas canvas) {
// mObjectAnimatorSweep動畫執行時,mCurrentSweepAngle由0~300逐漸增加
float startAngle;
float sweepAngle;
if (!mModeAppearing) {// 弧度減小
// mCurrentGlobalAngleOffset不變
// startAngle遞增
startAngle = mCurrentSweepAngle - mCurrentGlobalAngleOffset;
// sweepAngle遞減
sweepAngle = 360 - mCurrentSweepAngle - MIN_SWEEP_ANGLE;
} else {// 弧度增加
// mCurrentGlobalAngleOffset每次循環增加60
// startAngle不變
startAngle = -mCurrentGlobalAngleOffset;
// sweepAngle遞增
sweepAngle = MIN_SWEEP_ANGLE + mCurrentSweepAngle;
}
canvas.drawArc(fBounds, startAngle, sweepAngle, false, mPaint);
}
將成員變量mCurrentGlobalAngle添加到參數startAngle中,即完成了兩個動畫的組合。
Android實戰--簡單的模糊查詢
今天這一篇小案例模擬模糊查詢,即輸入一個字符,顯示手機對應的所有存在該字符的路徑。布局:
Android中使用自定義ViewGroup的總結
分類自定義Layout可以分為兩種情況。 自定義ViewGroup,創造出一些不同於LinearLayout,RelativeLayout等之類的ViewGroup。比
面試准備android(一)
在牛客(一個很多筆試面試交流的平台,感覺每天一套可以萌萌哒(☆_☆))上看到一個大神,簡直是offer收割機TAT,其面經中好多東西都是基礎,覺得自己有必要總結並學習我不
Android Fragment學習
android Fragments詳解 Fragment是activity的界面中的一部分或一種行為。你可以把多個Fragment們組合到一個activity中