編輯:關於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中,即完成了兩個動畫的組合。
今天這一篇小案例模擬模糊查詢,即輸入一個字符,顯示手機對應的所有存在該字符的路徑。布局:
分類自定義Layout可以分為兩種情況。 自定義ViewGroup,創造出一些不同於LinearLayout,RelativeLayout等之類的ViewGroup。比
在牛客(一個很多筆試面試交流的平台,感覺每天一套可以萌萌哒(☆_☆))上看到一個大神,簡直是offer收割機TAT,其面經中好多東西都是基礎,覺得自己有必要總結並學習我不
android Fragments詳解 Fragment是activity的界面中的一部分或一種行為。你可以把多個Fragment們組合到一個activity中