自定義view(二)----自定義動(dòng)畫View

上一篇文章中我們自定義了一個(gè)簡(jiǎn)單的TextView,算是自定義view里面最簡(jiǎn)單的,也只是一個(gè)自定義view的入門級(jí),我會(huì)在這一系列文章中逐漸去定義更復(fù)雜的自定義view。同樣,這篇文章也是以一個(gè)自定義view為例,該view仿照qq運(yùn)動(dòng)的圖標(biāo),先來看看效果圖。


1644818657609.gif

下面我們來一步步實(shí)現(xiàn)這個(gè)自定義view,以主要代碼分析,文章末尾會(huì)給出完整代碼,先理清一下實(shí)現(xiàn)它的思路,我把實(shí)現(xiàn)的過程分為一下七步:

  • 1.分析view的屬性
  • 2.自定義view屬性
  • 3.在布局文件中使用自定義屬性
  • 4.在自定義view中獲取自定義屬性
  • 5.重寫onMea()方法
  • 6.重寫onDraw()方法,畫外圓弧,內(nèi)圓弧,文字
  • 7.實(shí)現(xiàn)動(dòng)畫效果

準(zhǔn)備工作

創(chuàng)建一個(gè)類QQStepView繼承View類并實(shí)現(xiàn)它的所有構(gòu)造方法,在參照上篇文章所說,每個(gè)構(gòu)造內(nèi)部方法實(shí)現(xiàn)它的下一個(gè)構(gòu)造方法,這樣可以保證無論調(diào)用哪個(gè)構(gòu)造方法都能實(shí)現(xiàn)我們的業(yè)務(wù)邏輯,關(guān)于自定義view的每個(gè)構(gòu)造方法的說明,還有不了解的可以參照我的上一篇文章Android UI-----自定義view(一),實(shí)現(xiàn)類基本代碼如下

public class QQStepView extends View {
   

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

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

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

1、分析view的屬性

通過圖中可以看出,該view的屬性主要有三個(gè)顏色和兩個(gè)尺寸,三個(gè)顏色是指變化圓弧的顏色、整個(gè)大圓弧的顏色、字體顏色,兩個(gè)尺寸是圓弧的寬度尺寸、字體的大小尺寸。
所以該view的屬性主要有五個(gè),分別如下:

  • outerColor 大圓弧的顏色
  • innerColor 變化圓弧的顏色
  • stepTextColor 字體顏色
  • stepBorderWidth 圓弧的寬度
  • stepTextSize 字體的大小

2、自定義屬性

既然已經(jīng)把所有屬性都分析好了,那就自定義屬性,自定義屬性代碼如下,不知道如何創(chuàng)建自定義屬性文件的可以去看我的上一篇文章

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="QQStepView">
        <attr name="outerColor" format="color" />
        <attr name="innerColor" format="color" />
        <attr name="stepBorderWidth" format="dimension" />
        <attr name="stepTextSize" format="dimension" />
        <attr name="stepTextColor" format="color" />
    </declare-styleable>
</resources>

3、在布局文件中使用自定義屬性

這個(gè)沒什么好說的,代碼如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    tools:context=".MainActivity">
    <com.example.kotlindemo.QQStepView
        android:id="@+id/stepView2"
        android:layout_gravity="center"
        android:layout_width="200dp"
        android:layout_height="200dp"
        app:stepBorderWidth="6dp"
        app:innerColor="#FF1493"
        app:stepTextColor="#FF1493"
        app:stepTextSize="16sp"
        app:outerColor="#0000FF" />
</LinearLayout>

4、在自定義view中獲取自定義屬性

獲取自定義屬性在上一篇文章也已經(jīng)說得很清楚了,一般在創(chuàng)建的自定義view類的第三個(gè)構(gòu)造方法中使用

 public QQStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QQStepView);
        mOuterColor = array.getColor(R.styleable.QQStepView_outerColor, Color.RED);
        mInnerColor = array.getColor(R.styleable.QQStepView_innerColor, Color.BLUE);
        mStepBorderWidth = (int) array.getDimension(R.styleable.QQStepView_stepBorderWidth, 20);
        mStepTextSize = array.getDimensionPixelSize(R.styleable.QQStepView_stepTextSize, 20);
        mStepTextColor = array.getColor(R.styleable.QQStepView_stepTextColor, Color.RED);
        array.recycle();
    }

5、重寫onMea()方法

重寫onMea()的目的是為了測(cè)量寬高,但是在本例中我們需要實(shí)現(xiàn)兩個(gè)目的,一個(gè)是需要將該view的寬高控制為指定寬高,不能是wrap_content,另一個(gè)目的是控制view為正方形(寬高一致);詳情見代碼,有注釋

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //判斷寬高模式,確保為指定寬高
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (widthMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST) {
            Toast.makeText(mContext, "請(qǐng)給寬高指定確切值", Toast.LENGTH_LONG).show();
            return;
        }
        //如果寬高不一致,取最小值,保證是個(gè)正方形
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(width > height ? height : width, width > height ? height : width);
    }

6、.重寫onDraw()方法,畫外圓弧,內(nèi)圓弧,文字

關(guān)于重寫onDraw()方法,需要注意怎么去畫圓弧,畫文字就不多說了,前面也已經(jīng)講過,那怎么來畫圓弧呢?畫圓弧需要使用canvas.drawArc()方法

 RectF rectf = new RectF(50, 50, getWidth() - 2 * mStepBorderWidth, getHeight() - 2 * mStepBorderWidth);//繪制區(qū)域
        //另一種方法
        /*int center=getWidth()/2;//獲取中心點(diǎn)
        int radius=getWidth()/2-mStepBorderWidth/2;//獲取半徑
        RectF rectf=new RectF(center-radius,center-radius,center+radius,center+radius);//繪制區(qū)域*/
        canvas.drawArc(rectf, 135, 270, false, mOutPaint);

canvas.drawArc()接收五個(gè)參數(shù),第一個(gè)參數(shù)表示繪制的區(qū)域,第二個(gè)參數(shù)是繪畫開始的角度(由效果圖可知從135度開始繪畫),第三個(gè)參數(shù)表示需要跨越多少個(gè)角度(整個(gè)圓弧從頭到尾占了270度),第四個(gè)參數(shù)表示是否實(shí)心,我們只需要它的外圓弧,所以是false,最后一個(gè)參數(shù)是接收一個(gè)畫筆。
需要注意繪制區(qū)域的定義有兩種方法,我都給出來了,因?yàn)閳A弧本身是有寬度的,所以繪制區(qū)域需要重新測(cè)量,不然會(huì)超出最大區(qū)域,這一點(diǎn)是比較特殊的。
接下來是畫筆mOutPaint的一些屬性參數(shù),這個(gè)可以在第三個(gè)構(gòu)造函數(shù)中進(jìn)行初始化

 //初始化外圓弧畫筆
        mOutPaint = new Paint();
        mOutPaint.setAntiAlias(true);//抗鋸齒
        mOutPaint.setStrokeWidth(mStepBorderWidth);//畫筆寬度
        mOutPaint.setColor(mOuterColor);    //畫筆顏色
        mOutPaint.setStyle(Paint.Style.STROKE);//畫筆樣式空心
        mOutPaint.setStrokeCap(Paint.Cap.ROUND);//設(shè)置圓弧斷點(diǎn)處圓角

需要注意最后兩個(gè),一個(gè)是設(shè)置畫筆樣式空心,不設(shè)置的話會(huì)畫出一個(gè)圓餅,最后一個(gè)參數(shù)是為了使整個(gè)圓的斷開處是圓角。
畫變化的圓也是一樣的方法,只是我們從效果圖中可以看出,圓是變化的,證明它經(jīng)歷角度也是要根據(jù)占據(jù)大圓的角度來變化的,所以繪畫的代碼應(yīng)該是

 if (mStepMax == 0)//防止分母為0
            return;
        float sweepAngle = (float) mCurrentStep / mStepMax*270;
        canvas.drawArc(rectf, 135, sweepAngle , false, mInnerPaint);

我們可以看到第三個(gè)參數(shù)是一個(gè)百分比,mCurrentStep 代表當(dāng)前步數(shù)(變化弧形的值),mStepMax代表最大步數(shù)(大弧形的值),不要忘記乘以270。變化圓的畫筆屬性初始化和大圓弧的差不多,這里就不多說了。
下面是整個(gè)onDraw()完整代碼,畫文字沒啥好說的,前面一篇文章已經(jīng)分析過了。


@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //6.1畫外圓弧
        RectF rectf = new RectF(50, 50, getWidth() - 2 * mStepBorderWidth, getHeight() - 2 *  mStepBorderWidth);//繪制區(qū)域
        //另一種方法
        /*int center=getWidth()/2;//獲取中心點(diǎn)
        int radius=getWidth()/2-mStepBorderWidth/2;//獲取半徑
        RectF rectf=new RectF(center-radius,center-radius,center+radius,center+radius);//繪制區(qū)域*/
        canvas.drawArc(rectf, 135, 270, false, mOutPaint);
        //6.2畫內(nèi)圓弧 繪畫角度采用當(dāng)前步數(shù)與最大步數(shù)進(jìn)行百分比計(jì)算
        if (mStepMax == 0)//防止分母為0
            return;
        float sweepAngle = (float) mCurrentStep / mStepMax*270;
        canvas.drawArc(rectf, 135, sweepAngle , false, mInnerPaint);
        //畫文字
        String stepText = mCurrentStep + "";
        Rect textBounds = new Rect();
        mTextPaint.getTextBounds(stepText, 0, stepText.length(), textBounds);//測(cè)量文字畫筆長(zhǎng)度
        int dx = getWidth() / 2 - textBounds.width() / 2;
        Paint.FontMetricsInt fontMetricsInt = mTextPaint.getFontMetricsInt();
        int dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
        int baseLine = getHeight() / 2 + dy;
        canvas.drawText(stepText, dx, baseLine, mTextPaint);
    }

7.實(shí)現(xiàn)動(dòng)畫效果

實(shí)現(xiàn)動(dòng)畫效果最重要的就是當(dāng)前步數(shù)(變化圓弧的值)mCurrentStep 需要不斷變化,這樣上面講到的繪畫時(shí)它所跨越的角度sweepAngle就會(huì)不斷變化,從而繪畫出會(huì)變化的圓。
我們?cè)赒QStepView先定義兩個(gè)方法,方便對(duì)mCurrentStep 和mStepMax 進(jìn)行賦值

    public void setStepMax(int maxStep) {
        this.mStepMax = maxStep;
    }

    public void setCurrentStep(int currentStep) {
        this.mCurrentStep = currentStep;
        //不斷重繪
        invalidate();
    }

只是兩個(gè)簡(jiǎn)單的賦值方法,需要注意在mCurrentStep 的賦值方法中調(diào)用了 invalidate();方法,這是一個(gè)不斷重繪的方法,它會(huì)根據(jù)情況去調(diào)用自定義view的onDraw(),使整個(gè)view重新進(jìn)行繪制。
緊接著在MainActivity中實(shí)現(xiàn)屬性動(dòng)畫的代碼并添加監(jiān)聽事件,關(guān)于動(dòng)畫,不懂的可以自己學(xué)習(xí)下,這里不多講,屬性動(dòng)畫有時(shí)間的話我會(huì)單獨(dú)寫一篇文章介紹它。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        QQStepView qqStepView=findViewById(R.id.stepView);
        qqStepView.setStepMax(4000);
        ValueAnimator valueAnimator= ObjectAnimator.ofFloat(0,3000);
        valueAnimator.setDuration(2000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float currentStep=(float) animation.getAnimatedValue();
                qqStepView.setCurrentStep((int) currentStep);
            }
        });
        valueAnimator.start();
    }
}

所有文件完整代碼

QQStepView.java

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Toast;

import androidx.annotation.Nullable;

/**
 * 包名    com.example.kotlindemo
 * 作者    zzp
 * 創(chuàng)建時(shí)間 2022/2/9 11:08
 * 描述    仿照qq運(yùn)動(dòng)自定義view
 */
public class QQStepView extends View {
    private Context mContext;
    private Paint mOutPaint, mInnerPaint, mTextPaint;
    private int mCurrentStep = 50;//當(dāng)前步數(shù)
    private int mStepMax = 100;//最大步數(shù)
    //定義自定義屬性
    private int mOuterColor;
    private int mInnerColor;
    private int mStepBorderWidth;
    private int mStepTextSize;
    private int mStepTextColor;

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

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

    public QQStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //1.分析view的屬性
        //2.自定義view屬性
        //3.在布局文件中使用自定義屬性
        //4.在自定義view中獲取自定義屬性
        //5.重寫onMeasure()方法
        //6.重寫onDraw()方法,畫外圓弧,內(nèi)圓弧,文字
        //7.其他處理
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QQStepView);
        mOuterColor = array.getColor(R.styleable.QQStepView_outerColor, Color.RED);
        mInnerColor = array.getColor(R.styleable.QQStepView_innerColor, Color.BLUE);
        mStepBorderWidth = (int) array.getDimension(R.styleable.QQStepView_stepBorderWidth, 20);
        mStepTextSize = array.getDimensionPixelSize(R.styleable.QQStepView_stepTextSize, 20);
        mStepTextColor = array.getColor(R.styleable.QQStepView_stepTextColor, Color.RED);
        //初始化外圓弧畫筆
        mOutPaint = new Paint();
        mOutPaint.setAntiAlias(true);//抗鋸齒
        mOutPaint.setStrokeWidth(mStepBorderWidth);//畫筆寬度
        mOutPaint.setColor(mOuterColor);    //畫筆顏色
        mOutPaint.setStyle(Paint.Style.STROKE);//畫筆樣式空心
        mOutPaint.setStrokeCap(Paint.Cap.ROUND);//設(shè)置圓弧斷點(diǎn)處圓角
        //初始化內(nèi)圓弧畫筆
        mInnerPaint = new Paint();
        mInnerPaint.setAntiAlias(true);//抗鋸齒
        mInnerPaint.setStrokeWidth(mStepBorderWidth);//畫筆寬度
        mInnerPaint.setColor(mInnerColor);    //畫筆顏色
        mInnerPaint.setStyle(Paint.Style.STROKE);//畫筆樣式空心
        mInnerPaint.setStrokeCap(Paint.Cap.ROUND);//設(shè)置圓弧斷點(diǎn)處圓角
        //初始化文字畫筆
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);//抗鋸齒
        mTextPaint.setColor(mStepTextColor);    //畫筆顏色
        mTextPaint.setTextSize(mStepTextSize);
        array.recycle();
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //判斷寬高模式,確保為指定寬高
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (widthMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST) {
            Toast.makeText(mContext, "請(qǐng)給寬高指定確切值", Toast.LENGTH_LONG).show();
            return;
        }
        //如果寬高不一致,取最小值,保證是個(gè)正方形
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(width > height ? height : width, width > height ? height : width);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //6.1畫外圓弧
        RectF rectf = new RectF(50, 50, getWidth() - 2 * mStepBorderWidth, getHeight() - 2 * mStepBorderWidth);//繪制區(qū)域
        //另一種方法
        /*int center=getWidth()/2;//獲取中心點(diǎn)
        int radius=getWidth()/2-mStepBorderWidth/2;//獲取半徑
        RectF rectf=new RectF(center-radius,center-radius,center+radius,center+radius);//繪制區(qū)域*/
        canvas.drawArc(rectf, 135, 270, false, mOutPaint);
        //6.2畫內(nèi)圓弧 繪畫角度采用當(dāng)前步數(shù)與最大步數(shù)進(jìn)行百分比計(jì)算
        if (mStepMax == 0)//防止分母為0
            return;
        float sweepAngle = (float) mCurrentStep / mStepMax;
        canvas.drawArc(rectf, 135, sweepAngle * 270, false, mInnerPaint);
        //畫文字
        String stepText = mCurrentStep + "";
        Rect textBounds = new Rect();
        mTextPaint.getTextBounds(stepText, 0, stepText.length(), textBounds);//測(cè)量文字畫筆長(zhǎng)度
        int dx = getWidth() / 2 - textBounds.width() / 2;
        Paint.FontMetricsInt fontMetricsInt = mTextPaint.getFontMetricsInt();
        int dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom;
        int baseLine = getHeight() / 2 + dy;
        canvas.drawText(stepText, dx, baseLine, mTextPaint);
    }

    //7、寫幾個(gè)方法讓它動(dòng)起來
    public void setStepMax(int maxStep) {
        this.mStepMax = maxStep;
    }

    public void setCurrentStep(int currentStep) {
        this.mCurrentStep = currentStep;
        //不斷重繪
        invalidate();
    }
}

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="QQStepView">
        <attr name="outerColor" format="color" />
        <attr name="innerColor" format="color" />
        <attr name="stepBorderWidth" format="dimension" />
        <attr name="stepTextSize" format="dimension" />
        <attr name="stepTextColor" format="color" />
    </declare-styleable>
</resources>

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        QQStepView qqStepView=findViewById(R.id.stepView);
        qqStepView.setStepMax(4000);
        ValueAnimator valueAnimator= ObjectAnimator.ofFloat(0,3000);
        valueAnimator.setDuration(2000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float currentStep=(float) animation.getAnimatedValue();
                qqStepView.setCurrentStep((int) currentStep);
            }
        });
        valueAnimator.start();
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
   android:gravity="center"
    tools:context=".MainActivity">


    <com.example.kotlindemo.QQStepView
        android:id="@+id/stepView"
        android:layout_gravity="center"
        android:layout_width="200dp"
        android:layout_height="200dp"
        app:stepBorderWidth="6dp"
        app:innerColor="#FF1493"
        app:stepTextColor="#FF1493"
        app:stepTextSize="16sp"
        app:outerColor="#0000FF" />

</LinearLayout>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容