Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 開源項目circular-progress-button源碼分析

開源項目circular-progress-button源碼分析

編輯:關於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// 錯誤
}

(2).成員變量初始化
CircularProgressButton在構造方法中完成對成員變量的初始化操作。
通過getResources().getColorStateList(id)方法,根據id獲取ColorStateList對象(參數id是在xml中定義的selector,其中包含了各個不同狀態下的顏色值),然後賦值給相應的ColorStateList對象。再通過colorStateList.getColorForState(int[] stateSet, int defaultColor)方法取出各個狀態下的color,調用stateListDrawable.addState(int[] stateSet, Drawable drawable)將顏色值添加到三個StateListDrawable對象中。參數stateSet同在xml中設置的state一致,包括state_enabled、state_focused、state_pressed等。
// 初始化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類。
在上面morph()方法中,執行按鈕變換使用了MorphingAnimation類。按鈕在不同狀態之間的變換依賴此類來實現。該類通過各種參數,寬度、圓角、描邊、內間距等,執行ValueAnimator動畫,來改變按鈕的背景色,圓角半徑和描邊顏色。執行ValueAnimator動畫代碼如下。
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)方法
在setProgress(int progress)方法中,當mProgress > IDLE_STATE_PROGRESS且mProgress < mMaxProgress,mState == State.PROGRESS時,直接執行invalidate()方法,觸發onDraw(Canvas canvas)方法的調用,在onDraw(Canvas canvas)中來繪制圓環進度的變化。
圓環進度又分為兩種,一直循環的進度和根據progress值繪制的進度,分別由drawIndeterminateProgress(Canvas canvas)和drawProgress(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類
在drawProgress(Canvas canvas)方法中使用了該類,根據當前的進度值,來繪制圓形進度條。內部的實現主要是重寫draw(Canvas canvas)方法,使用android.graphics.Path類繪制弧形。
@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類
在drawIndeterminateProgress(Canvas canvas)方法中使用了該類,來繪制一直循環轉動的圓形進度條。
CircularAnimatedDrawable由兩個動畫組合執行來繪制圓形進度,分別是mObjectAnimatorAngle和mObjectAnimatorSweep。
動畫的執行可以分解開來看。采用注釋掉代碼的方式,一次只允許一個動畫執行。會發現,只允許mObjectAnimatorAngle執行時,圓弧勻速旋轉,弧度不變;只允許mObjectAnimatorSweep執行時,圓弧不旋轉,但是弧度一直在發生改變。

 

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°的藍色)。
我們將動畫的時間延長10倍,可以清晰的看到,圓弧最初顯示的是最大弧度,圓弧的弧度減小,直到最小弧度為止。然後圓弧的弧度增加,直到最大弧度為止。接下來再進入上一步變化,依次循環下去。如果只分解出mObjectAnimatorSweep動畫產生的繪制,onDraw()相當於如下代碼。
@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中,即完成了兩個動畫的組合。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved