自定义View?一起来打造一个圆形进度条吧。

闲聊

说起自定义View,可能有些人就有点头疼,甚至有点恐惧,包括我自己。对于很多 Android 入门的程序员来说,自定义 View 是一道难以跨越的坎,但是如果你想成为高手,自定义 View 又是必须要熟练掌握的,所以没办法,坎再难过,也只能硬着头皮上了。以前我总是畏惧自定义 View 这一部分知识点,遇到这部分的知识点自己总是选择性的跳过,但是现在不行了。为了找一份好点的工作,再难也要克服下来。

先说一下自定义 View 的几个步骤吧。

自定义View属性
在构造方法中获取自定义属性并进行初始化工作
重写 onMeasure 方法
重写 onDraw 方法

先不用管第三步的重写 onMeasure 方法,这个方法设计到测量工作,说起来可能有些难以理解,现在我们暂时不管 onMeasure 方法。

好了,言归正传,我们标题说是要自定义一个圆形的进度条,那么开始的示意图还是要有一个的,毕竟 no pic say 个 jj。
演示图

自定义属性

嗯,上面已经有了 jj, 下面就开始我们的老规矩,自定义三部曲。首先自定义一下圆形进度条的属性。从上面的演示图中我们就可以看到有如下几个属性

1. 背景圆的颜色
2. 背景圆的半径大小
3. 显示百分比的文字颜色
4. 显示百分比文字的大小
5. 进度条的颜色
6. 当前进度

暂时只有这些属性,后面如果需要添加属性,我们在去自定义一下,没什么问题。在 values/attrs.xml 文件中添加如下代码

<!--自定义View属性 -->
<declare-styleable name="CircleProgress">
    <attr name="cricleBg" format="color"/>
    <attr name="radius" format="dimension"/>
    <attr name="precentColor" format="color"/>
    <attr name="precentTextSize" format="dimension"/>
    <attr name="progressWidth" format="dimension"/>
</declare-styleable>

我这里好像只写了 5 个属性,嗯,没多大关系。ps:上面的代码看不懂的,请自行百度。

新建CustomView类。

新建一个 CustomView 类,并将该类继承至 View。显然,我们需要重写几个构造方法。

public CircleView(Context context) {
        this(context, null);
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
}
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
}

喏,几个构造方法都在这里了,我这里通过 this 关键字将几个构造函数串联在一起了。还有一个 4 个参数的构造函数我没有重写,主要是几乎用不到那个构造函数,所以我并没有重写那个构造函数。

下面我们做一些准备工作。毕竟磨刀不误砍柴工,杀牛也要先磨刀嘛。初始化工作主要是初始化画笔,设置画笔的颜色,宽度等。

//设置背景圆的画笔属性
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);//去锯齿
mCirclePaint.setStyle(Paint.Style.FILL);//设置画笔方式为填充
mCirclePaint.setColor(circleBg);//设置画笔颜色

//设置文字画笔属性
mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//去锯齿
mTextPaint.setStyle(Paint.Style.STROKE);//设置画笔方式为线条
mTextPaint.setColor(precentColor);//设置文字画笔颜色
mTextPaint.setTextSize(precentTextsize);//设置文字大小

//设置进度条画笔
mArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//初始化画笔,去锯齿
mArcPaint.setStyle(Paint.Style.STROKE);//设置画笔样式为线条
mArcPaint.setColor(Color.BLUE);//设置画笔颜色
mArcPaint.setStrokeWidth(progressWidth);//设置画笔线条宽度
mArcPaint.setStrokeCap(Paint.Cap.ROUND);//设置线条两端为圆形

获取自定义属性

嗯,对了,我们好像之前有自定义过属性,所以我们在初始化画笔之前,应该需要先获取我们自定义的属性。

那获取自定义属性用什么方式呢? 当然是 TypeArray 啦。至于怎么获取,请看代码。

//获取自定义属性的值,也就是初始化自定义View的值
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleProgress);

precentTextsize = array.getDimensionPixelSize(R.styleable.CircleProgress_precentTextSize, ToolsUtil.sp2px(context, 16));

precentColor = array.getColor(R.styleable.CircleProgress_precentColor, Color.BLACK);

circleBg = array.getColor(R.styleable.CircleProgress_cricleBg, Color.RED);

radius = array.getDimensionPixelSize(R.styleable.CircleProgress_radius, ToolsUtil.dip2px(context, 100));

progressWidth = array.getDimensionPixelSize(R.styleable.CircleProgress_progressWidth, ToolsUtil.dip2px(context, 3));

array.recycle(); //切记,这步必须要调用,否则我们自定义的属性将不会生效

以上就是获取我们在 attrs.xml 文件中的自定义属性的方式。

重写 onDraw 方法

好了,我们的刀已经磨好了,现在给我把猪牵过来,我要宰了它。

####第一步
首先,我们先使用画板的 drawCircle 方法画出底部的一个圆。

canvas.drawCircle(cx,cy, radius, paint);

根据我们 6 的飞起的数学知识来看,我们要话一个圆,首先要有圆心,其次再是半径,我们才能画一个圆出来。而上面的方法也正是验证了我们的猜想,cx 代表圆心的横坐标,cy 代表圆心的纵坐标,radius 代表圆的半径,最后的
paint 肯定是画笔啦。什么?你不知道?食屎啦?

正好,半径和画笔我们早就已经初始化过了,所以我们直接把初始化过的半径和画笔传进去就好了。至于圆心怎么确定?这里我们取屏幕的正中间为圆心吧。

下面我们重写 onSizeChanged 这个方法。我偷偷告诉你,这个方法的参数中的 h 就是屏幕的高度,w 就是屏幕的宽度,所以取中心?这还不简单吗?圆心已经呼之欲出了。cx 为 w/2,cy 为 h/2。 好了,至此,我们的背景圆已经画出来了,看看我们的成果。
圆形进度条的背景

简直美美哒。奖励一下自己(给自己一巴掌)

第二步

背景圆我们已经画出来了,已经成功了一半,现在来画最外面的进度弧形。画弧形嘛。简直 so easy。画板 canvas 已经帮我们提供了画弧形的方法。

画圆弧的方法

上面提供了两种方法,显然第二种方法参数比较少,也相对简单,我们这里采用第二种方式,第二种方式接受 5 个参数。

RectF oval,//这个参数表示我们要画的整个圆弧的外接圆的四边属性
float startAngle, //画圆弧的起始角度
float sweepAngle,//圆弧转过的角度
boolean userCenter,//暂时不解释,到时候看图就知道什么意思了
Paint paint,//显然是画笔

第三个参数我暂时不做解释,反正他是 boolean 类型,只有 true 和 false 之分,什么状况我们到时候一试便知。
先来搞定 RectF 这个东西,其实这个东西还有一个孪生兄弟 Rect。这个好理解,Rect 就是正方形,通过 new 方式生成实例,这里面的参数 分别是 left, top, right, bottom,类型均是 int, RectF 参数跟 Rect 一样,只不过它的参数类型是 float 类型。既然画圆需要这个东西,那我们就如他所愿,通过 new 方式生成 RectF 的实例。

之前我说了 RectF 是圆弧的外接圆,那么对应的左上右下分别是 w/2 - radius,h/2 - radius,w/2 + radius, h/2 + radius。不明白什么情况的就动动小手,画一下草图,很容易看出来的。

第二个参数是起始角度,由于我们是从竖直方向上开始画的圆弧,所以起始角度应该是 270度,这里需要注意水平方向,也就是 x 轴正方向才是 0度(360度)。

第三个参数是圆弧转过的角度,我们已经知道了圆弧占比,假如 precent 为 30,那么进度应该是 30 / 100 * 100% 就应该是30%,一个整圆是360度,占比 30% 就是 108度。所以第三个参数应该是 precent / 100 * 360。

第四个参数我们先设置成 false。

第五个参数就是画笔咯,早前我们就已经初始化过,这里直接传入画圆弧的画笔实例即可。

mCircle = new RectF(); //背景圆的外接矩形
mCircle.set(
         getWidth() / 2 - radius + 10 / 2,
         getHeight() / 2 - radius + 10 / 2,
         getWidth() / 2 + radius - 10 / 2,
         getHeight() / 2 + radius - 10 / 2);
canvas.drawArc(mCircle, 270, 360 * mCurPercent / 100, false, mArcPaint);

这里有个细节,就是我们在给 mCircle 赋值的时候,并非是按照上面的方式,因为我们的圆弧是覆盖在背景圆的边缘上,而并非是贴合在圆弧边缘上,所以此时我们还需要针对实际情况对圆弧的宽度和 left, top, right,bottom 进行一些处理。

万事俱备,就差效果了,准备了这么久,应该看一下效果。

圆弧效果图

嗯,完美。这里的圆弧是根据我们传入的百分数动态计算出来的。下面就差一个百分比了。

####第三步
画背景圆中间的百分比。
同上,我们同样需要知道文本对应的外接圆,根据外接圆来调用如下方法画出文本。
这里我就不做过多解释了,你们直接看代码吧。很简单的,也比较容易理解。

String text = mCurPercent + "%";
//计算文本宽高
mTextPaint.getTextBounds(text, 0, String.valueOf(text).length(), mText);
//画百分比文本
canvas.drawText(text, getWidth() / 2 - mText.width() / 2
                , getHeight() / 2 + mText.height() / 2, mTextPaint);
                
第四步

暴露相关成员变量,给外界提供 set 方法去动态设置参数属性。同时添加属性动画。我这里只暴露了一个设置百分比的方法,其他的成员变量你们可以自行添加。

public void setPrecent(float curPercent){
        ValueAnimator anim = ValueAnimator.ofFloat(mCurPercent, curPercent);
        //动画时长由百分比大小决定
        anim.setDuration((long) (Math.abs(mCurPercent - curPercent) * 20));
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                mCurPercent = (float) (Math.round(value * 10)) / 10;//四舍五入保留到小数点后两位
                invalidate();
            }
        });
        anim.start();
    }

到此为止,我们的自定义圆形进度条就完成了。下面贴出全部的源代码。

CircleView.java

public class CircleView extends View {

    private int precentTextsize;
    private int precentColor;
    private int radius;
    private int circleBg;
    private int progressWidth;

    private RectF mCircle;
    private Rect mText;

    private float mCurPercent = 3;

    private Paint mCirclePaint; //圆形背景画笔
    private Paint mTextPaint; //百分比文字画笔
    private Paint mArcPaint; //边缘进度画笔


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

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

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


        //获取自定义属性的值,也就是初始化自定义View的值
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleProgress);
        precentTextsize = array.getDimensionPixelSize(R.styleable.CircleProgress_precentTextSize, ToolsUtil.sp2px(context, 16));
        precentColor = array.getColor(R.styleable.CircleProgress_precentColor, Color.BLACK);
        circleBg = array.getColor(R.styleable.CircleProgress_cricleBg, Color.RED);
        radius = array.getDimensionPixelSize(R.styleable.CircleProgress_radius, ToolsUtil.dip2px(context, 100));
        progressWidth = array.getDimensionPixelSize(R.styleable.CircleProgress_progressWidth, ToolsUtil.dip2px(context, 3));

        array.recycle();

        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCirclePaint.setStyle(Paint.Style.FILL);
        mCirclePaint.setColor(circleBg);

        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setStyle(Paint.Style.STROKE);
        mTextPaint.setColor(precentColor);
        mTextPaint.setTextSize(precentTextsize);


        mArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mArcPaint.setStyle(Paint.Style.STROKE);
        mArcPaint.setColor(Color.BLUE);
        mArcPaint.setStrokeWidth(progressWidth);
        mArcPaint.setStrokeCap(Paint.Cap.ROUND);


        mCircle = new RectF(); //背景圆的外接矩形
        mText = new Rect(); //百分比文字的外接矩形

    }

    //绘制
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(getWidth()/2, getHeight()/2, radius, mCirclePaint);


        //画圆弧
        mCircle.set(
                getWidth() / 2 - radius + 10 / 2,
                getHeight() / 2 - radius + 10 / 2,
                getWidth() / 2 + radius - 10 / 2,
                getHeight() / 2 + radius - 10 / 2);



        canvas.drawArc(mCircle, 270, 360 * mCurPercent / 100, false, mArcPaint);


        String text = mCurPercent + "%";


        //计算文本宽高
        mTextPaint.getTextBounds(text, 0, String.valueOf(text).length(), mText);
        //画百分比文本
        canvas.drawText(text, getWidth() / 2 - mText.width() / 2
                , getHeight() / 2 + mText.height() / 2, mTextPaint);



    }




    public void setPrecent(float curPercent){
        ValueAnimator anim = ValueAnimator.ofFloat(mCurPercent, curPercent);
        //动画时长由百分比大小决定
        anim.setDuration((long) (Math.abs(mCurPercent - curPercent) * 20));
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                mCurPercent = (float) (Math.round(value * 10)) / 10;//四舍五入保留到小数点后两位
                invalidate();
            }
        });
        anim.start();
    }
}

布局文件(注意别忘了名称空间)

 <com.student.customview.CircleView
        android:visibility="visible"
        android:id="@+id/circle_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:precentTextSize="20sp"
        app:precentColor="#dfdf9d"
        app:radius="100dp"
        app:progressWidth="10dp"/>

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private CircleView mCircleView;
    private int precent = 13;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mCircleView = (CircleView) findViewById(R.id.circle_view);
        mCircleView.setPrecent(precent);

    }
    public void click(View view){
        if(precent <= 90){
            precent = precent + 10;
        }else{
            precent = 0;
        }
        mCircleView.setPrecent(precent);
    }
}

最后

这个 Demo 仅仅只供学习自定义 View 使用。切勿盲目的使用在工作项目中。能力有限,难免写出来的东西会有错误,如有错误,还请各位大神不吝赐教,不胜感激。如有疑问,欢迎交流。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值