Android進階:九、自定義View之手寫Loading動效

Android丶SE開發發表於2019-04-30

這是一個很簡單的動畫效果,使用屬性動畫即可實現,希望對讀者學習動畫能達到拋磚引玉的效果

一.自定義動畫效果——Loading效果

如上是我們需要做的一個Loading動畫。Loading效果是很常見的一種動畫,最簡單的實現讓設計畫個動態圖即可,或者畫個靜態圖然後使用幀動畫也可以實現。但是今天我們用純程式碼實現,不用任何圖片資源。

Android進階:九、自定義View之手寫Loading動效


Android進階:九、自定義View之手寫Loading動效

大致思路
我們自定義一個View,繼承View類,然後畫兩個不同半徑的弧形,轉動不同的角度即可實現。

繪製兩個不同半徑的弧形
首先初始化外圓和內園的Recf();

    private RectF mOuterCircleRectF = new RectF();
    private RectF mInnerCircleRectF = new RectF();
複製程式碼

然後在onDraw方法繪製圓弧:

        //獲取View的中心
        float centerX = getWidth() / 2;
        float centerY = getHeight() / 2;

        if (lineWidth > centerX) {
            throw new IllegalArgumentException("lineWidth值太大了");
        }
        //外圓半徑,因為我們的弧形是有寬度的,所以計算半徑的時候應該把這部分減去,不然會有切割的效果
        float outR = centerX - lineWidth;

        //小圓半徑
        float inR = outR * 0.6f - lineWidth;
        //設定弧形的距離上下左右的距離,也就是包圍園的矩形。
        mOuterCircleRectF.set(centerX - outR, centerY - outR, centerX + outR, centerY + outR);
        mInnerCircleRectF.set(centerX - inR, centerY - inR, centerX + inR, centerY + inR);
        //繪製外圓
        canvas.drawArc(mOuterCircleRectF, mRotateAngle % 360, OUTER_CIRCLE_ANGLE, false, mStrokePaint);
        //繪製內圓
        canvas.drawArc(mInnerCircleRectF, 270 - mRotateAngle % 360, INTER_CIRCLE_ANGLE, false, mStrokePaint);
複製程式碼

程式碼很簡單,就像註釋一樣:

  • 獲取整個loadView的寬高,然後計算loadview的中心
  • 利用中心計算外圓和內園的半徑,因為圓弧的弧邊有寬度,所以應該減去這部分寬度,不然上下左右會有被切割的效果。
  • 在Recf中設定以圓半徑為邊長的矩形
  • 在畫布中以矩形的資料繪製圓弧即可,這裡設定了角度,使圓形有缺角,只要不是360度的圓都是有缺角的。

繪製圓的過程應該放在onDraw方法中,這樣我們可以不斷的重繪,也可以獲取view的真實的寬高

當然,我們還需設定一個畫筆來畫我們的圓

       mStrokePaint = new Paint();
        mStrokePaint.setStyle(Paint.Style.STROKE);
        mStrokePaint.setStrokeWidth(lineWidth);
        mStrokePaint.setColor(color);
        mStrokePaint.setAntiAlias(true);
        mStrokePaint.setStrokeCap(Paint.Cap.ROUND);
        mStrokePaint.setStrokeJoin(Paint.Join.ROUND);
複製程式碼

二.設定屬性動畫

圓弧畫好了,然後利用屬性動畫即可實現動畫效果。這裡採用的是ValueAnimator,值屬性動畫,我們可以設定一個值範圍,然後讓他在這個範圍內變化。

     mFloatValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
        mFloatValueAnimator.setRepeatCount(Animation.INFINITE);
        mFloatValueAnimator.setDuration(ANIMATION_DURATION);
        mFloatValueAnimator.setStartDelay(ANIMATION_START_DELAY);
        mFloatValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
複製程式碼

這個設定很簡單,設定值得範圍,這是無線迴圈,設定動畫執行的時間,這隻動畫迴圈時延遲的時間,設定插值器。

三.弧形動起來

讓弧形動起來的原理,就是監聽值屬性動畫的值變化,然後在這個變化的過程中不斷的改變弧形的角度,然後讓它重繪即可。

我們讓我們的loadview實現ValueAnimator.AnimatorUpdateListener介面,然後在onAnimationUpdate監聽動畫的變化。我們初始化值屬性動畫的時候設定了值得範圍為float型,所以這裡可以獲取這個變化的值。然後利用這個值可以改變繪製圓的角度大小,再呼叫重繪方法,即可實現:

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        mRotateAngle = 360 * (float)animation.getAnimatedValue();
        invalidate();
    }
複製程式碼

整個思路大致就是這樣。完整程式碼如下:

public class LoadingView extends View implements Animatable, ValueAnimator.AnimatorUpdateListener {
    private static final long ANIMATION_START_DELAY = 200;
    private static final long ANIMATION_DURATION = 1000;
    private static final int OUTER_CIRCLE_ANGLE = 270;
    private static final int INTER_CIRCLE_ANGLE = 90;

    private ValueAnimator mFloatValueAnimator;
    private Paint mStrokePaint;
    private RectF mOuterCircleRectF;
    private RectF mInnerCircleRectF;

    private float mRotateAngle;


    public LoadingView (Context context) {
        this(context, null);
    }

    public LoadingView (Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public LoadingView (Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, -1);
    }

    public LoadingView (Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView(context, attrs);
    }

    float lineWidth;

    private void initView(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyCustomLoadingView);
        lineWidth = typedArray.getFloat(R.styleable.MyCustomLoadingView_lineWidth, 10.0f);
        int color = typedArray.getColor(R.styleable.MyCustomLoadingView_viewColor, context.getColor(R.color.colorAccent));
        typedArray.recycle();
        initAnimators();
        mOuterCircleRectF = new RectF();
        mInnerCircleRectF = new RectF();
        //初始化畫筆
        initPaint(lineWidth, color);
        //旋轉角度
        mRotateAngle = 0;
    }

    private void initAnimators() {
        mFloatValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
        mFloatValueAnimator.setRepeatCount(Animation.INFINITE);
        mFloatValueAnimator.setDuration(ANIMATION_DURATION);
        mFloatValueAnimator.setStartDelay(ANIMATION_START_DELAY);
        mFloatValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
    }

    /**
     * 初始化畫筆
     */
    private void initPaint(float lineWidth, int color) {
        mStrokePaint = new Paint();
        mStrokePaint.setStyle(Paint.Style.STROKE);
        mStrokePaint.setStrokeWidth(lineWidth);
        mStrokePaint.setColor(color);
        mStrokePaint.setAntiAlias(true);
        mStrokePaint.setStrokeCap(Paint.Cap.ROUND);
        mStrokePaint.setStrokeJoin(Paint.Join.ROUND);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float centerX = getWidth() / 2;
        float centerY = getHeight() / 2;

        //最大尺寸
        if (lineWidth > centerX) {
            throw new IllegalArgumentException("lineWidth值太大了");
        }
        float outR = centerX - lineWidth;
        //小圓尺寸
        float inR = outR * 0.6f;
        mOuterCircleRectF.set(centerX - outR, centerY - outR, centerX + outR, centerY + outR);
        mInnerCircleRectF.set(centerX - inR, centerY - inR, centerX + inR, centerY + inR);
        //先儲存畫板的狀態
        canvas.save();
        //外圓
        canvas.drawArc(mOuterCircleRectF, mRotateAngle % 360, OUTER_CIRCLE_ANGLE, false, mStrokePaint);
        //內圓
        canvas.drawArc(mInnerCircleRectF, 270 - mRotateAngle % 360, INTER_CIRCLE_ANGLE, false, mStrokePaint);
        //恢復畫板的狀態
        canvas.restore();
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        startLoading();
    }

    public void startLoading() {
        start();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stopLoading();
    }

    public void stopLoading() {
        stop();
    }

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        mRotateAngle = 360 * (float)animation.getAnimatedValue();
        invalidate();
    }

    protected void computeUpdateValue(float animatedValue) {
        mRotateAngle = (int) (360 * animatedValue);
    }

    @Override
    public void start() {
        if (mFloatValueAnimator.isStarted()) {
            return;
        }
        mFloatValueAnimator.addUpdateListener(this);
        mFloatValueAnimator.setRepeatCount(Animation.INFINITE);
        mFloatValueAnimator.setDuration(ANIMATION_DURATION);
        mFloatValueAnimator.start();
    }

    @Override
    public void stop() {
        mFloatValueAnimator.removeAllUpdateListeners();
        mFloatValueAnimator.removeAllListeners();
        mFloatValueAnimator.setRepeatCount(0);
        mFloatValueAnimator.setDuration(0);
        mFloatValueAnimator.end();
    }

    @Override
    public boolean isRunning() {
        return mFloatValueAnimator.isRunning();
    }
}
複製程式碼

attr檔案程式碼如下:

    <declare-styleable name="LoadingView">
        <attr name="lineWidth" format="float" />
        <attr name="viewColor" format="color" />
    </declare-styleable>複製程式碼

如果喜歡我的文章,想與一群資深開發者一起交流學習的話,歡迎加入我的合作群Android Senior Engineer技術交流群。有flutter—效能優化—移動架構—資深UI工程師 —NDK相關專業人員和視訊教學資料,後面也有和本篇文章想對應的視訊資料分享
群號:925019412


相關文章