自定義View_手擼一個啟動頁倒數計時View

weixin_34208283發表於2017-08-08

在一個APP啟動的時候呢,一般經常見到倒數計時3秒或幾秒的場景,在這個場景中,也經常看到一個有動畫載入的view,比如下面今天要實現的效果圖:放個GitHub傳送門先:CountDownView

6112330-31549e14dfc51ecc.gif
countdownview.gif

分析


正所謂知己知彼百戰百勝,所以我們每去做一件事情之前都要去花費一定的時間去了解一些相關的東西。那麼這樣的一個效果呢其實不難,我們只需兩個東西即可實現。——canvas和屬性動畫。

1.自定義我們需要的屬性:


那麼為了考慮擴充套件性,那麼有些屬性呢我們不能寫死,自定義屬性是最好的選擇!首先在values資料夾下新建檔案attrs.xml

  <declare-styleable name="CountDownView">
        <!--view半徑-->
        <attr name="cd_circle_radius" format="dimension" />
        <!--畫筆寬度-->
        <attr name="cd_arc_width" format="dimension" />
        <!--畫筆顏色-->
        <attr name="cd_arc_color" format="color" />
        <!--背景顏色-->
        <attr name="cd_bg_color" format="color" />
        <!--字型顏色-->
        <attr name="cd_text_color" format="color" />
        <!--字型尺寸-->
        <attr name="cd_text_size" format="dimension" />
        <!--動畫執行時長-->
        <attr name="cd_animator_time" format="integer" />
        <!--時間單位-->
        <attr name="cd_animator_time_unit" format="string" />
        <!--動畫進退方式-->
        <attr name="cd_retreat_type" format="enum">
            <!--外層的圓弧逐漸變長-->
            <enum name="forward" value="1" />
            <!--外層的圓弧逐漸減短-->
            <enum name="back" value="2" />
        </attr>
        <!--載入進度的開始位置-->
        <attr name="cd_location" format="enum">
            <enum name="left" value="1" />
            <enum name="top" value="2" />
            <enum name="right" value="3" />
            <enum name="bottom" value="4" />
        </attr>
    </declare-styleable>

然後在自定義View中獲取並設定這些屬性:
首先,來宣告和獲取定義好的屬性:

    private Paint mPaintBackGround;//背景畫筆
    private Paint mPaintArc;//圓弧畫筆
    private Paint mPaintText;//文字畫筆
    private int mRetreatType;//圓弧繪製方式(增加和減少)
    private float mPaintArcWidth;//最外層圓弧的寬度
    private int mCircleRadius;//圓圈的半徑
    private int mPaintArcColor = Color.parseColor("#3C3F41");//初始值
    private int mPaintBackGroundColor = Color.parseColor("#55B2E5");//初始值
    private int mLoadingTime;//時間,單位秒
    private String mLoadingTimeUnit = "";//時間單位
    private int mTextColor = Color.BLACK;//字型顏色
    private int mTextSize;//字型大小
    private int location;//從哪個位置開始
    private float startAngle;//開始角度
    private float mmSweepAngleStart;//起點
    private float mmSweepAngleEnd;//終點
    private float mSweepAngle;//掃過的角度
    private String mText = "";//要繪製的文字

獲取這些屬性值:

TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CountDownView);
mRetreatType = array.getInt(R.styleable.CountDownView_cd_retreat_type, 1);
location = array.getInt(R.styleable.CountDownView_cd_location, 1);
mCircleRadius = (int) array.getDimension(R.styleable.CountDownView_cd_circle_radius, dip2px(context, 25));//預設25dp
mPaintArcWidth = array.getDimension(R.styleable.CountDownView_cd_arc_width, dip2px(context, 3));//預設3dp
mPaintArcColor = array.getColor(R.styleable.CountDownView_cd_arc_color, mPaintArcColor);
mTextSize = (int) array.getDimension(R.styleable.CountDownView_cd_text_size, dip2px(context, 14));//預設14sp
mTextColor = array.getColor(R.styleable.CountDownView_cd_text_color, mTextColor);
mPaintBackGroundColor = array.getColor(R.styleable.CountDownView_cd_bg_color, mPaintBackGroundColor);
mLoadingTime = array.getInteger(R.styleable.CountDownView_cd_animator_time, 3);//預設3秒
mLoadingTimeUnit = array.getString(R.styleable.CountDownView_cd_animator_time_unit);//時間單位
if (TextUtils.isEmpty(mLoadingTimeUnit)) {
    mLoadingTimeUnit = "";
}
array.recycle();

初始化畫筆等操作:

    private void init() {
        //背景設為透明,然後造成Views是圓形視覺錯覺
        this.setBackground(ContextCompat.getDrawable(mContext, android.R.color.transparent));
        mPaintBackGround = new Paint();
        mPaintBackGround.setStyle(Paint.Style.FILL);
        mPaintBackGround.setAntiAlias(true);
        mPaintBackGround.setColor(mPaintBackGroundColor);

        mPaintArc = new Paint();
        mPaintArc.setStyle(Paint.Style.STROKE);
        mPaintArc.setAntiAlias(true);
        mPaintArc.setColor(mPaintArcColor);
        mPaintArc.setStrokeWidth(mPaintArcWidth);

        mPaintText = new Paint();
        mPaintText.setStyle(Paint.Style.STROKE);
        mPaintText.setAntiAlias(true);
        mPaintText.setColor(mTextColor);
        mPaintText.setTextSize(mTextSize);
        if (mLoadingTime < 0) {
            mLoadingTime = 3;
        }
        if (location == 1) {//預設從左側開始
            startAngle = -180;
        } else if (location == 2) {
            startAngle = -90;
        } else if (location == 3) {
            startAngle = 0;
        } else if (location == 4) {
            startAngle = 90;
        }

        if (mRetreatType == 1) {
            mmSweepAngleStart = 0f;
            mmSweepAngleEnd = 360f;
        } else {
            mmSweepAngleStart = 360f;
            mmSweepAngleEnd = 0f;
        }
    }

2.畫出需要的效果:畫圓弧,畫字型,畫背景:


這裡我們使用cancas的drawArc()方法,不瞭解這個方法是什麼意思的請跳至此處檢視詳細解釋~drawArc()方法詳細介紹

    //畫北景園
    canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2 - mPaintArcWidth, mPaintBackGround);
    //畫圓弧
    RectF rectF = new RectF(0 + mPaintArcWidth / 2, 0 + mPaintArcWidth / 2
            , mWidth - mPaintArcWidth / 2, mHeight - mPaintArcWidth / 2);
    canvas.drawArc(rectF, startAngle, mSweepAngle, false, mPaintArc);
    //畫文字
    float mTetxWidth = mPaintText.measureText(mText, 0, mText.length());
    float dx = mWidth / 2 - mTetxWidth / 2;
    Paint.FontMetricsInt fontMetricsInt = mPaintText.getFontMetricsInt();
    float dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
    float baseLine = mHeight / 2 + dy;
    canvas.drawText(mText, dx, baseLine, mPaintText);

3.改變屬性值,重新繪製;


這一步就是關於屬性動畫的知識了。

public void start() {
    ValueAnimator animator = ValueAnimator.ofFloat(mmSweepAngleStart, mmSweepAngleEnd);
    animator.setInterpolator(new LinearInterpolator());
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            mSweepAngle = (float) valueAnimator.getAnimatedValue();
            //獲取到需要繪製的角度,重新繪製
            invalidate();
        }
    });
    //這裡是時間獲取和賦值
    ValueAnimator animator1 = ValueAnimator.ofInt(mLoadingTime, 0);
    animator1.setInterpolator(new LinearInterpolator());
    animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            int time = (int) valueAnimator.getAnimatedValue();
            mText = time + mLoadingTimeUnit;
        }
    });
    AnimatorSet set = new AnimatorSet();
    set.playTogether(animator, animator1);
    set.setDuration(mLoadingTime * 1000);
    set.setInterpolator(new LinearInterpolator());
    set.start();
    set.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            clearAnimation();
            if (loadingFinishListener != null) {
                loadingFinishListener.finish();
            }
        }
    });
}

4.介面回撥。


這一步就很簡單了,只要監聽動畫執行結束就是完成了載入,所以我們先來寫一個介面。

    private OnLoadingFinishListener loadingFinishListener;

    public void setOnLoadingFinishListener(OnLoadingFinishListener listener) {
        this.loadingFinishListener = listener;
    }

    public interface OnLoadingFinishListener {
        void finish();
    }

在對應的Activity中回撥介面就可以了。

OK,到這裡就算全部結束了,下面我把原始碼放進來。

CountDownView.java:

package com.zhuyong.countdownciew;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;

import com.zhuyong.counttimeview.R;

/**
 * Created by zhuyong on 2017/8/7.
 * 啟動頁停留n秒動畫view
 */

public class CountDownView extends View {
    private Context mContext;//上下文
    private Paint mPaintBackGround;//背景畫筆
    private Paint mPaintArc;//圓弧畫筆
    private Paint mPaintText;//文字畫筆
    private int mRetreatType;//圓弧繪製方式(增加和減少)
    private float mPaintArcWidth;//最外層圓弧的寬度
    private int mCircleRadius;//圓圈的半徑
    private int mPaintArcColor = Color.parseColor("#3C3F41");//初始值
    private int mPaintBackGroundColor = Color.parseColor("#55B2E5");//初始值
    private int mLoadingTime;//時間,單位秒
    private String mLoadingTimeUnit = "";//時間單位
    private int mTextColor = Color.BLACK;//字型顏色
    private int mTextSize;//字型大小
    private int location;//從哪個位置開始
    private float startAngle;//開始角度
    private float mmSweepAngleStart;//起點
    private float mmSweepAngleEnd;//終點
    private float mSweepAngle;//掃過的角度
    private String mText = "";//要繪製的文字
    private int mWidth;
    private int mHeight;

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

    public CountDownView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CountDownView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;

        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CountDownView);
        mRetreatType = array.getInt(R.styleable.CountDownView_cd_retreat_type, 1);
        location = array.getInt(R.styleable.CountDownView_cd_location, 1);
        mCircleRadius = (int) array.getDimension(R.styleable.CountDownView_cd_circle_radius, dip2px(context, 25));//預設25dp
        mPaintArcWidth = array.getDimension(R.styleable.CountDownView_cd_arc_width, dip2px(context, 3));//預設3dp
        mPaintArcColor = array.getColor(R.styleable.CountDownView_cd_arc_color, mPaintArcColor);
        mTextSize = (int) array.getDimension(R.styleable.CountDownView_cd_text_size, dip2px(context, 14));//預設14sp
        mTextColor = array.getColor(R.styleable.CountDownView_cd_text_color, mTextColor);
        mPaintBackGroundColor = array.getColor(R.styleable.CountDownView_cd_bg_color, mPaintBackGroundColor);
        mLoadingTime = array.getInteger(R.styleable.CountDownView_cd_animator_time, 3);//預設3秒
        mLoadingTimeUnit = array.getString(R.styleable.CountDownView_cd_animator_time_unit);//時間單位
        if (TextUtils.isEmpty(mLoadingTimeUnit)) {
            mLoadingTimeUnit = "";
        }
        array.recycle();
        init();
    }

    private void init() {
        //背景設為透明,然後造成圓形View的視覺錯覺
        this.setBackground(ContextCompat.getDrawable(mContext, android.R.color.transparent));
        mPaintBackGround = new Paint();
        mPaintBackGround.setStyle(Paint.Style.FILL);
        mPaintBackGround.setAntiAlias(true);
        mPaintBackGround.setColor(mPaintBackGroundColor);

        mPaintArc = new Paint();
        mPaintArc.setStyle(Paint.Style.STROKE);
        mPaintArc.setAntiAlias(true);
        mPaintArc.setColor(mPaintArcColor);
        mPaintArc.setStrokeWidth(mPaintArcWidth);

        mPaintText = new Paint();
        mPaintText.setStyle(Paint.Style.STROKE);
        mPaintText.setAntiAlias(true);
        mPaintText.setColor(mTextColor);
        mPaintText.setTextSize(mTextSize);
        if (mLoadingTime < 0) {
            mLoadingTime = 3;
        }
        if (location == 1) {//預設從左側開始
            startAngle = -180;
        } else if (location == 2) {
            startAngle = -90;
        } else if (location == 3) {
            startAngle = 0;
        } else if (location == 4) {
            startAngle = 90;
        }

        if (mRetreatType == 1) {
            mmSweepAngleStart = 0f;
            mmSweepAngleEnd = 360f;
        } else {
            mmSweepAngleStart = 360f;
            mmSweepAngleEnd = 0f;
        }
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //獲取view寬高
        mWidth = w;
        mHeight = h;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //因為必須是圓形的view,所以在這裡重新賦值
        setMeasuredDimension(mCircleRadius * 2, mCircleRadius * 2);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //畫北景園
        canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2 - mPaintArcWidth, mPaintBackGround);
        //畫圓弧
        RectF rectF = new RectF(0 + mPaintArcWidth / 2, 0 + mPaintArcWidth / 2
                , mWidth - mPaintArcWidth / 2, mHeight - mPaintArcWidth / 2);
        canvas.drawArc(rectF, startAngle, mSweepAngle, false, mPaintArc);
        //畫文字
        float mTetxWidth = mPaintText.measureText(mText, 0, mText.length());
        float dx = mWidth / 2 - mTetxWidth / 2;
        Paint.FontMetricsInt fontMetricsInt = mPaintText.getFontMetricsInt();
        float dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
        float baseLine = mHeight / 2 + dy;
        canvas.drawText(mText, dx, baseLine, mPaintText);

    }


    public void start() {
        ValueAnimator animator = ValueAnimator.ofFloat(mmSweepAngleStart, mmSweepAngleEnd);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mSweepAngle = (float) valueAnimator.getAnimatedValue();
                //獲取到需要繪製的角度,重新繪製
                invalidate();
            }
        });
        //這裡是時間獲取和賦值
        ValueAnimator animator1 = ValueAnimator.ofInt(mLoadingTime, 0);
        animator1.setInterpolator(new LinearInterpolator());
        animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int time = (int) valueAnimator.getAnimatedValue();
                mText = time + mLoadingTimeUnit;
            }
        });
        AnimatorSet set = new AnimatorSet();
        set.playTogether(animator, animator1);
        set.setDuration(mLoadingTime * 1000);
        set.setInterpolator(new LinearInterpolator());
        set.start();
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                clearAnimation();
                if (loadingFinishListener != null) {
                    loadingFinishListener.finish();
                }
            }
        });

    }

    private OnLoadingFinishListener loadingFinishListener;

    public void setOnLoadingFinishListener(OnLoadingFinishListener listener) {
        this.loadingFinishListener = listener;
    }

    public interface OnLoadingFinishListener {
        void finish();
    }

    /**
     * 根據手機的解析度從 dp 的單位 轉成為 px(畫素)
     */
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
}

我把這個view封裝成了library和上傳,用得到的朋友可以直接線上依賴到自己的專案中,至於如何整合和使用,請看GitHub:CountDownView

好的,至此全部結束,感謝!!!

相關文章