自定義view————loading框

madreain發表於2017-07-31

寫自定義view,怎麼能沒有自己寫的loading框了,今天給大家送上一個loading框,前期losing框是很簡單的效果,三個狀態:loading、success、error,不逼逼了,先上給糙一點的效果GIF,回頭繼續完善。

效果圖
效果圖

糙一點的loading框效果看完了,接下來說一下實現過程,採用WindowManager將losing框的view置於顯示view的上層,起初上來是loading轉圈狀態,根據載入返回的結構設定成功或者失敗。

loading狀態

ValueAnimator實現AnimatorUpdateListener、AnimatorListener監聽方法,AnimatorUpdateListener中接受valueAnimator.getAnimatedValue();(不斷回撥的在0-1這個範圍內,經過插值器插值之後的返回值),然後去畫圈圈

這裡畫的是一個通過改變不同起始角度畫給一個240度的圓弧,以此達到loading效果

    mPath = new Path();
    loadingrectF = new RectF(-100, -100, 100, 100);
    mPath.addArc(loadingrectF, mAnimatorValue * 360, 240);
    canvas.drawPath(mPath, mPaint);
    canvas.drawColor(Color.parseColor("#33000000"));複製程式碼

mAnimatorValue值的變化是AnimatorUpdateListener函式裡面回撥的值,然後通知重繪


loadingvalueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                // 不斷回撥的在0-1這個範圍內,經過插值器插值之後的返回值
                mAnimatorValue = (float) valueAnimator.getAnimatedValue();
                 //重繪
                invalidate();

            }
        });複製程式碼

為了保證轉了一圈過後繼續轉,這裡引進Handler,在AnimatorListener的結束方法中進行通知動畫繼續

loadingvalueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {

            }

            @Override
            public void onAnimationEnd(Animator animator) {
                // getHandle發訊息通知動畫狀態更新
                mAnimatorHandler.sendEmptyMessage(0);
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });複製程式碼

附上Handler相關程式碼


mAnimatorHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch (mcurrentState) {
                    case LODING:
                        //保持loading時一直執行動畫
                        loadingvalueAnimator.start();
                        break;
                    case SUCCES:
                        break;
                    case ERROR:
                        break;
                    default:
                        break;
                }

            }
        };複製程式碼

到此為止,loading就能一直轉了,接下來就是success、error兩個狀態了的講解了

success狀態

成功狀態時,loading消失,顯示成功的相關提醒(原諒我是一個程式設計師,做的loading不好看,準備後期功能完善後,會把loading、success、error狀態的顯示弄的像UI設計的一樣),廢話不多說了,該上菜了


  /**
     * 成功
     */
    public void setSuccess() {
        loadingvalueAnimator.removeAllListeners();
        errorvalueAnimator.removeAllListeners();
        addSuccesLoadingListener();
        successvalueAnimator.start();
    }複製程式碼

移除掉其他ValueAnimator對應的監聽方法,為success加上ValueAnimator的監聽方法,這裡就是顯示一下success狀態的文案,保持500ms,然後顯示(這一塊可以改成其他動畫酷炫效果)


successvalueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
                //動畫開始,去繪製success的文案
                mcurrentState = State.SUCCES;
                invalidate();
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                //動畫結束,隱藏狀態
                LoadingView.this.setVisibility(GONE);
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });複製程式碼

error狀態

和success狀態同理,只是文案不一樣

程式碼中使用

新增到activity開始loading動畫,待結果返回時然後設定相關狀態顯示500ms,然後顯示消失,接下來貼出模擬的loading--->success--->dismiss的一個過程

              loadingRoundView = new LoadingView(this);
              loadingRoundView.addPartentViewStartLoading(MainActivity.this);

              mErrorHandler = new Handler() {
                  @Override
                  public void handleMessage(Message msg) {
                      super.handleMessage(msg);
                      if (error <= 50) {
                          if (error == 0) {

                              loadingRoundView.startLoading();

                          }
                          error += 1;
                          mErrorHandler.sendEmptyMessageDelayed(0, 100);
                      } else {
                          loadingRoundView.setError();

                      }
                  }
              };
      //
              mSuccesHandler = new Handler() {
                  @Override
                  public void handleMessage(Message msg) {
                      super.handleMessage(msg);
                      if (succes <= 50) {
                          succes += 1;
                          mSuccesHandler.sendEmptyMessageDelayed(0, 100);
                      } else {
                          loadingRoundView.setSuccess();

                          mErrorHandler.sendEmptyMessageDelayed(0, 1000);
                      }
                  }
              };

              mSuccesHandler.sendEmptyMessageDelayed(0, 100);複製程式碼

設定loading方式

目前只寫了三個loading方式

//loading的型別
    private Type mcurrentType = Type.ROUND;

    //loading type
    public enum Type {
        ARC,//傳統弧形轉圈
        CIRCLE,//天女散花
        ROUND,//漸變的圓圈旋轉
    }複製程式碼

然後根據不同的loading方法,ondraw()裡面執行的方法也不一樣

case LODING:
                if (mcurrentType == Type.ARC) {
                    mPath = new Path();
                    RectF loadingrectF = new RectF(-radius, -radius, radius, radius);
                    mPath.addArc(loadingrectF, mAnimatorValue * 360, 240);
                    canvas.drawPath(mPath, mPaint);
                } else if (mcurrentType == Type.CIRCLE) {
                    mPaint.setStyle(Paint.Style.FILL);
                    mPath = new Path();
                    for (int i = 0; i < 10; i++) {
                        mPath.addCircle(mAnimatorValue * mwidth / 2 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 2 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
                        mPath.addCircle(mAnimatorValue * mwidth / 3 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 3 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
                        mPath.addCircle(mAnimatorValue * mwidth / 4 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 4 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
                        mPath.addCircle(mAnimatorValue * mwidth / 5 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 5 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
                        mPath.addCircle(mAnimatorValue * mwidth / 6 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 6 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
                    }
                    canvas.drawPath(mPath, mPaint);
                } else if (mcurrentType == Type.ROUND) {
                    mPaint.setStyle(Paint.Style.FILL);
                    Path path = new Path();
                    path.addCircle(0, 0, radius, Path.Direction.CW);           // 新增一個圓形
                    PathMeasure pathMeasure = new PathMeasure(path, false);
                    pathMeasure.getPosTan(pathMeasure.getLength() * mAnimatorValue, pos, tan);

//              mPath = new Path();
                    //使用 Math.atan2(tan[1], tan[0]) 將 tan 轉化為角(單位為弧度)的時候要注意引數順序。
                    float angle = (float) Math.atan2(tan[1], tan[0]);
                    for (int i = 0; i < roundCount; i++) {
                        //用path一次性畫的,透明度不好設定
//              mPath.addCircle((float) (Math.cos(angle + i*0.4) * 100), (float) (Math.sin(angle+ i*0.4) * 100), 16, Path.Direction.CW);
                        mPaint.setAlpha(onealpha * i);
                        canvas.drawCircle((float) (Math.cos(angle + i * 0.4) * radius), (float) (Math.sin(angle + i * 0.4) * radius), circleRadius, mPaint);
                    }
//              canvas.drawPath(mPath, circlePaint);
                }

                break;複製程式碼

附上loadingview完整程式碼

package com.loadingview;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;


/**
 * Created by wujun on 2017/7/31.
 *
 * @author madreain
 * @desc loading框  目前支援三種狀態及三種樣式
 */

public class LoadingView extends View {
    //螢幕的寬高
    int mwidth;
    int mheight;
    int backgroudColor;

    //主
    Paint mPaint;
    //屬性
    int mpaintColor;
    float mpaintStrokeWidth;

    Path mPath;
    //轉圈的半徑
    float radius = 100;
    //畫圓圈的半徑
    float circleRadius = 16;
    //設定轉圈的圓點數量
    private int roundCount = 10;
    //1-255
    private int onealpha;

    private float[] pos;                // 當前點的實際位置
    private float[] tan;                // 當前點的tangent值,用於計算圖片所需旋轉的角度

    //loading的動效
    ValueAnimator loadingvalueAnimator;
    //loading 動畫數值(用於控制動畫狀態,因為同一時間內只允許有一種狀態出現,具體數值處理取決於當前狀態)
    private float mAnimatorValue = 0;

    //loading 用於控制動畫狀態轉換
    private Handler mAnimatorHandler;

    State mcurrentState = State.LODING;

    //loading狀態,loading,成功,失敗
    private enum State {
        LODING,
        SUCCES,
        ERROR,
    }

    //成功失敗的畫筆
    Paint textPaint;
    int textPaintColor;
    float textPaintStrokeWidth;
    float textPaintTextSize;

    //成功的動效
    ValueAnimator successvalueAnimator;
    //失敗的動效
    ValueAnimator errorvalueAnimator;


    //loading的型別
    private Type mcurrentType = Type.ROUND;

    //loading type
    public enum Type {
        ARC,//傳統弧形轉圈
        CIRCLE,//天女散花
        ROUND,//漸變的圓圈旋轉
    }

    public LoadingView(Context context) {
        super(context);
        initPaint();
        initHandler();
        initListener();
    }


    public LoadingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initTypedArray(context, attrs);
        initPaint();
        initHandler();
        initListener();
    }

    private void initTypedArray(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
        backgroudColor=typedArray.getColor(R.styleable.LoadingView_backgroudColor,Color.parseColor("#66000000"));
        mpaintColor = typedArray.getColor(R.styleable.LoadingView_mpaintColor, Color.BLUE);
        mpaintStrokeWidth = typedArray.getFloat(R.styleable.LoadingView_mpaintStrokeWidth, 16);
        textPaintColor = typedArray.getColor(R.styleable.LoadingView_textPaintColor, Color.BLUE);
        textPaintStrokeWidth = typedArray.getFloat(R.styleable.LoadingView_textPaintStrokeWidth, 6);
        textPaintTextSize = typedArray.getFloat(R.styleable.LoadingView_textPaintTextSize, 60);
        radius = typedArray.getFloat(R.styleable.LoadingView_radius, 100);
        circleRadius = typedArray.getFloat(R.styleable.LoadingView_circleRadius, 16);
        roundCount = typedArray.getInteger(R.styleable.LoadingView_roundCount, 10);
        int type = typedArray.getInt(R.styleable.LoadingView_Type, 0);
        if (type == 0) {
            mcurrentType = Type.ARC;
        } else if (type == 1) {
            mcurrentType = Type.CIRCLE;
        } else if (type == 2) {
            mcurrentType = Type.ROUND;
        }
    }


    public LoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initTypedArray(context, attrs);
        initPaint();
        initHandler();
        initListener();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mwidth = w;
        mheight = h;
    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(mpaintColor);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStrokeWidth(mpaintStrokeWidth);
        mPaint.setStyle(Paint.Style.STROKE);

        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(textPaintColor);
        textPaint.setStrokeCap(Paint.Cap.ROUND);
        textPaint.setStrokeWidth(textPaintStrokeWidth);
        textPaint.setStyle(Paint.Style.STROKE);
        textPaint.setTextSize(textPaintTextSize);

        pos = new float[2];
        tan = new float[2];

        //計算透明度
        onealpha = 255 / roundCount;

    }

    private void initHandler() {
        mAnimatorHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch (mcurrentState) {
                    case LODING:
                        //保持loading時一直執行動畫
                        loadingvalueAnimator.start();
                        break;
                    case SUCCES:
                        break;
                    case ERROR:
                        break;
                    default:
                        break;
                }

            }
        };
    }

    private void initListener() {
        // 建立0-1的一個過程,任何複雜的過程都可以採用歸一化,然後在addUpdateListener回撥裡去做自己想要的變化
        loadingvalueAnimator = ValueAnimator.ofFloat(0, 1);
        // 設定過程的時間為2S
        loadingvalueAnimator.setDuration(2000);

        successvalueAnimator = ValueAnimator.ofFloat(0, 1);
        successvalueAnimator.setDuration(500);

        errorvalueAnimator = ValueAnimator.ofFloat(0, 1);
        errorvalueAnimator.setDuration(500);
    }

    private void addLoadingListener() {
        loadingvalueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                // 不斷回撥的在0-1這個範圍內,經過插值器插值之後的返回值
                mAnimatorValue = (float) valueAnimator.getAnimatedValue();
                // 獲取當前點座標封裝到mCurrentPosition
//                mPathMeasure.getPosTan(mAnimatorValue, mCurrentPosition, null);
                //重繪
                invalidate();

            }
        });

        loadingvalueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {

            }

            @Override
            public void onAnimationEnd(Animator animator) {
                // getHandle發訊息通知動畫狀態更新
                mAnimatorHandler.sendEmptyMessage(0);
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });
    }

    private void addSuccesLoadingListener() {
//        successvalueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
//            @Override
//            public void onAnimationUpdate(ValueAnimator valueAnimator) {
//
//
//            }
//        });

        successvalueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
                mcurrentState = State.SUCCES;
                invalidate();
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                //動畫結束,隱藏狀態
                LoadingView.this.setVisibility(GONE);
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });
    }

    private void addErrorLoadingListener() {
//        errorvalueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
//            @Override
//            public void onAnimationUpdate(ValueAnimator valueAnimator) {
//
//
//            }
//        });

        errorvalueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
                mcurrentState = State.ERROR;
                invalidate();
            }

            @Override
            public void onAnimationEnd(Animator animator) {
                //動畫結束,隱藏狀態
                LoadingView.this.setVisibility(GONE);
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //移到螢幕中間
        canvas.translate(mwidth / 2, mheight / 2);
        //都新增背景
        canvas.drawColor(backgroudColor);
        drawLoading(canvas);

    }

    private void drawLoading(Canvas canvas) {
        switch (mcurrentState) {
            case LODING:
                if (mcurrentType == Type.ARC) {
                    mPath = new Path();
                    RectF loadingrectF = new RectF(-radius, -radius, radius, radius);
                    mPath.addArc(loadingrectF, mAnimatorValue * 360, 240);
                    canvas.drawPath(mPath, mPaint);
                } else if (mcurrentType == Type.CIRCLE) {
                    mPaint.setStyle(Paint.Style.FILL);
                    mPath = new Path();
                    for (int i = 0; i < 10; i++) {
                        mPath.addCircle(mAnimatorValue * mwidth / 2 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 2 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
                        mPath.addCircle(mAnimatorValue * mwidth / 3 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 3 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
                        mPath.addCircle(mAnimatorValue * mwidth / 4 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 4 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
                        mPath.addCircle(mAnimatorValue * mwidth / 5 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 5 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
                        mPath.addCircle(mAnimatorValue * mwidth / 6 * (float) Math.cos(30 * i), mAnimatorValue * mwidth / 6 * (float) Math.sin(30 * i), 16, Path.Direction.CW);
                    }
                    canvas.drawPath(mPath, mPaint);
                } else if (mcurrentType == Type.ROUND) {
                    mPaint.setStyle(Paint.Style.FILL);
                    Path path = new Path();
                    path.addCircle(0, 0, radius, Path.Direction.CW);           // 新增一個圓形
                    PathMeasure pathMeasure = new PathMeasure(path, false);
                    pathMeasure.getPosTan(pathMeasure.getLength() * mAnimatorValue, pos, tan);

//              mPath = new Path();
                    //使用 Math.atan2(tan[1], tan[0]) 將 tan 轉化為角(單位為弧度)的時候要注意引數順序。
                    float angle = (float) Math.atan2(tan[1], tan[0]);
                    for (int i = 0; i < roundCount; i++) {
                        //用path一次性畫的,透明度不好設定
//              mPath.addCircle((float) (Math.cos(angle + i*0.4) * 100), (float) (Math.sin(angle+ i*0.4) * 100), 16, Path.Direction.CW);
                        mPaint.setAlpha(onealpha * i);
                        canvas.drawCircle((float) (Math.cos(angle + i * 0.4) * radius), (float) (Math.sin(angle + i * 0.4) * radius), circleRadius, mPaint);
                    }
//              canvas.drawPath(mPath, circlePaint);
                }

                break;
            case SUCCES:

                canvas.drawCircle(0, 0, radius, textPaint);

                canvas.drawLine(-radius / 2, 0, 0, radius / 2, textPaint);
                canvas.drawLine(0, radius / 2, radius / 2, -radius / 2, textPaint);

                break;
            case ERROR:
                canvas.drawCircle(0, 0, radius, textPaint);

                canvas.drawLine(radius / 2, radius / 2, -radius / 2, -radius / 2, textPaint);
                canvas.drawLine(radius / 2, -radius / 2, -radius / 2, radius / 2, textPaint);

                break;
            default:
                break;
        }
    }


    /**
     * 新增到activity的上層並執行動畫
     * @param activity
     *
     */
    public void addPartentViewStartLoading(Activity activity) {
        ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        //Activity中View佈局的最祖宗佈局,是一個FrameLayout,叫做DecorView,通過getWindow().getDecorView()可以獲取到
        FrameLayout view = (FrameLayout) activity.getWindow().getDecorView();
        view.addView(this,layoutParams);
        startLoading();
    }


    /**
     * 設定loading開始
     */
    public void startLoading() {
        if (loadingvalueAnimator != null) {
            mcurrentState = State.LODING;
            addLoadingListener();
            successvalueAnimator.removeAllListeners();
            errorvalueAnimator.removeAllListeners();
            //如果是gone --->VISIBLE
            if (this.getVisibility() == GONE) {
                this.setVisibility(VISIBLE);
            }
            loadingvalueAnimator.start();
        }
    }

    /**
     * 成功
     */
    public void setSuccess() {
        loadingvalueAnimator.removeAllListeners();
        errorvalueAnimator.removeAllListeners();
        addSuccesLoadingListener();
        successvalueAnimator.start();
    }

    /**
     * 失敗
     */
    public void setError() {
        loadingvalueAnimator.removeAllListeners();
        successvalueAnimator.removeAllListeners();
        addErrorLoadingListener();
        errorvalueAnimator.start();
    }

    /**
     * 整個事件的消費來保證loading狀態不可操作
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return true;
    }

    /**
     * 設定loading的背景顏色
     * @param backgroudColor
     */
    public void setBackgroudColor(int backgroudColor) {
        this.backgroudColor = backgroudColor;
    }

    /**
     *
     * @param mpaintColor
     */
    public void setMpaintColor(int mpaintColor) {
        this.mpaintColor = mpaintColor;
    }

    /**
     *
     * @param mpaintStrokeWidth
     */
    public void setMpaintStrokeWidth(float mpaintStrokeWidth) {
        this.mpaintStrokeWidth = mpaintStrokeWidth;
    }

    /**
     *
     * @param textPaintColor
     */
    public void setTextPaintColor(int textPaintColor) {
        this.textPaintColor = textPaintColor;
    }

    /**
     *
     * @param textPaintStrokeWidth
     */
    public void setTextPaintStrokeWidth(float textPaintStrokeWidth) {
        this.textPaintStrokeWidth = textPaintStrokeWidth;
    }

    /**
     *
     * @param textPaintTextSize
     */
    public void setTextPaintTextSize(float textPaintTextSize) {
        this.textPaintTextSize = textPaintTextSize;
    }

    /**
     *
     * @param radius
     */
    public void setRadius(float radius) {
        this.radius = radius;
    }

    /**
     *
     * @param circleRadius
     */
    public void setCircleRadius(float circleRadius) {
        this.circleRadius = circleRadius;
    }

    /**
     *
     * @param roundCount
     */
    public void setRoundCount(int roundCount) {
        this.roundCount = roundCount;
    }

    /**
     *
     * @param mcurrentType
     */
    public void setType(Type mcurrentType){
        this.mcurrentType=mcurrentType;
    }

}複製程式碼

檢視更多精彩請檢視個人部落格

最後為大家獻上自定義view————loading框 github demo,後期會將loading框優化,新增各種樣式

相關文章