自定义view(二)----自定义动画View

上一篇文章中我们自定义了一个简单的TextView,算是自定义view里面最简单的,也只是一个自定义view的入门级,我会在这一系列文章中逐渐去定义更复杂的自定义view。同样,这篇文章也是以一个自定义view为例,该view仿照qq运动的图标,先来看看效果图。


1644818657609.gif

下面我们来一步步实现这个自定义view,以主要代码分析,文章末尾会给出完整代码,先理清一下实现它的思路,我把实现的过程分为一下七步:

  • 1.分析view的属性
  • 2.自定义view属性
  • 3.在布局文件中使用自定义属性
  • 4.在自定义view中获取自定义属性
  • 5.重写onMea()方法
  • 6.重写onDraw()方法,画外圆弧,内圆弧,文字
  • 7.实现动画效果

准备工作

创建一个类QQStepView继承View类并实现它的所有构造方法,在参照上篇文章所说,每个构造内部方法实现它的下一个构造方法,这样可以保证无论调用哪个构造方法都能实现我们的业务逻辑,关于自定义view的每个构造方法的说明,还有不了解的可以参照我的上一篇文章Android UI-----自定义view(一),实现类基本代码如下

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的属性主要有三个颜色和两个尺寸,三个颜色是指变化圆弧的颜色、整个大圆弧的颜色、字体颜色,两个尺寸是圆弧的宽度尺寸、字体的大小尺寸。
所以该view的属性主要有五个,分别如下:

  • outerColor 大圆弧的颜色
  • innerColor 变化圆弧的颜色
  • stepTextColor 字体颜色
  • stepBorderWidth 圆弧的宽度
  • stepTextSize 字体的大小

2、自定义属性

既然已经把所有属性都分析好了,那就自定义属性,自定义属性代码如下,不知道如何创建自定义属性文件的可以去看我的上一篇文章

<?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、在布局文件中使用自定义属性

这个没什么好说的,代码如下

<?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中获取自定义属性

获取自定义属性在上一篇文章也已经说得很清楚了,一般在创建的自定义view类的第三个构造方法中使用

 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()的目的是为了测量宽高,但是在本例中我们需要实现两个目的,一个是需要将该view的宽高控制为指定宽高,不能是wrap_content,另一个目的是控制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, "请给宽高指定确切值", Toast.LENGTH_LONG).show();
            return;
        }
        //如果宽高不一致,取最小值,保证是个正方形
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        setMeasuredDimension(width > height ? height : width, width > height ? height : width);
    }

6、.重写onDraw()方法,画外圆弧,内圆弧,文字

关于重写onDraw()方法,需要注意怎么去画圆弧,画文字就不多说了,前面也已经讲过,那怎么来画圆弧呢?画圆弧需要使用canvas.drawArc()方法

 RectF rectf = new RectF(50, 50, getWidth() - 2 * mStepBorderWidth, getHeight() - 2 * mStepBorderWidth);//绘制区域
        //另一种方法
        /*int center=getWidth()/2;//获取中心点
        int radius=getWidth()/2-mStepBorderWidth/2;//获取半径
        RectF rectf=new RectF(center-radius,center-radius,center+radius,center+radius);//绘制区域*/
        canvas.drawArc(rectf, 135, 270, false, mOutPaint);

canvas.drawArc()接收五个参数,第一个参数表示绘制的区域,第二个参数是绘画开始的角度(由效果图可知从135度开始绘画),第三个参数表示需要跨越多少个角度(整个圆弧从头到尾占了270度),第四个参数表示是否实心,我们只需要它的外圆弧,所以是false,最后一个参数是接收一个画笔。
需要注意绘制区域的定义有两种方法,我都给出来了,因为圆弧本身是有宽度的,所以绘制区域需要重新测量,不然会超出最大区域,这一点是比较特殊的。
接下来是画笔mOutPaint的一些属性参数,这个可以在第三个构造函数中进行初始化

 //初始化外圆弧画笔
        mOutPaint = new Paint();
        mOutPaint.setAntiAlias(true);//抗锯齿
        mOutPaint.setStrokeWidth(mStepBorderWidth);//画笔宽度
        mOutPaint.setColor(mOuterColor);    //画笔颜色
        mOutPaint.setStyle(Paint.Style.STROKE);//画笔样式空心
        mOutPaint.setStrokeCap(Paint.Cap.ROUND);//设置圆弧断点处圆角

需要注意最后两个,一个是设置画笔样式空心,不设置的话会画出一个圆饼,最后一个参数是为了使整个圆的断开处是圆角。
画变化的圆也是一样的方法,只是我们从效果图中可以看出,圆是变化的,证明它经历角度也是要根据占据大圆的角度来变化的,所以绘画的代码应该是

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

我们可以看到第三个参数是一个百分比,mCurrentStep 代表当前步数(变化弧形的值),mStepMax代表最大步数(大弧形的值),不要忘记乘以270。变化圆的画笔属性初始化和大圆弧的差不多,这里就不多说了。
下面是整个onDraw()完整代码,画文字没啥好说的,前面一篇文章已经分析过了。


@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //6.1画外圆弧
        RectF rectf = new RectF(50, 50, getWidth() - 2 * mStepBorderWidth, getHeight() - 2 *  mStepBorderWidth);//绘制区域
        //另一种方法
        /*int center=getWidth()/2;//获取中心点
        int radius=getWidth()/2-mStepBorderWidth/2;//获取半径
        RectF rectf=new RectF(center-radius,center-radius,center+radius,center+radius);//绘制区域*/
        canvas.drawArc(rectf, 135, 270, false, mOutPaint);
        //6.2画内圆弧 绘画角度采用当前步数与最大步数进行百分比计算
        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);//测量文字画笔长度
        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.实现动画效果

实现动画效果最重要的就是当前步数(变化圆弧的值)mCurrentStep 需要不断变化,这样上面讲到的绘画时它所跨越的角度sweepAngle就会不断变化,从而绘画出会变化的圆。
我们在QQStepView先定义两个方法,方便对mCurrentStep 和mStepMax 进行赋值

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

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

只是两个简单的赋值方法,需要注意在mCurrentStep 的赋值方法中调用了 invalidate();方法,这是一个不断重绘的方法,它会根据情况去调用自定义view的onDraw(),使整个view重新进行绘制。
紧接着在MainActivity中实现属性动画的代码并添加监听事件,关于动画,不懂的可以自己学习下,这里不多讲,属性动画有时间的话我会单独写一篇文章介绍它。

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
 * 创建时间 2022/2/9 11:08
 * 描述    仿照qq运动自定义view
 */
public class QQStepView extends View {
    private Context mContext;
    private Paint mOutPaint, mInnerPaint, mTextPaint;
    private int mCurrentStep = 50;//当前步数
    private int mStepMax = 100;//最大步数
    //定义自定义属性
    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()方法,画外圆弧,内圆弧,文字
        //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);//设置圆弧断点处圆角
        //初始化内圆弧画笔
        mInnerPaint = new Paint();
        mInnerPaint.setAntiAlias(true);//抗锯齿
        mInnerPaint.setStrokeWidth(mStepBorderWidth);//画笔宽度
        mInnerPaint.setColor(mInnerColor);    //画笔颜色
        mInnerPaint.setStyle(Paint.Style.STROKE);//画笔样式空心
        mInnerPaint.setStrokeCap(Paint.Cap.ROUND);//设置圆弧断点处圆角
        //初始化文字画笔
        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, "请给宽高指定确切值", Toast.LENGTH_LONG).show();
            return;
        }
        //如果宽高不一致,取最小值,保证是个正方形
        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);//绘制区域
        //另一种方法
        /*int center=getWidth()/2;//获取中心点
        int radius=getWidth()/2-mStepBorderWidth/2;//获取半径
        RectF rectf=new RectF(center-radius,center-radius,center+radius,center+radius);//绘制区域*/
        canvas.drawArc(rectf, 135, 270, false, mOutPaint);
        //6.2画内圆弧 绘画角度采用当前步数与最大步数进行百分比计算
        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);//测量文字画笔长度
        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、写几个方法让它动起来
    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>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,183评论 6 516
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,850评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,766评论 0 361
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,854评论 1 299
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,871评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,457评论 1 311
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,999评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,914评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,465评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,543评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,675评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,354评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,029评论 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,514评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,616评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,091评论 3 378
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,685评论 2 360

推荐阅读更多精彩内容