【自定义View】带动画的百分比圆型控件

前言

这个View其实非常的简单,之所以写这个博客,一方面是记录下所学,另一方面是开启我的博客之旅。

效果示例

image

效果分析

这个效果还是非常简单的,就是画圆、画字外加一个属性动画的结果

既然是自定义View,那我们就按自定义View的流程来介绍:

1.继承合适的View,复写构造方法,初始化一些变量

2.自定义view的宽高测量 onMeasure 方法

3.自定义view的绘制 onDraw 方法

4.属性动画效果

开发流程

自定义View之继承

因为本控件层级不复杂,就是一个单独的View,不需要继承ViewGroup,直接继承View就行。

在这一步,除了继承之外,还需要做一些成员变量的创建和初始化

比如控件的宽高、内圆和外圆的画笔及其画笔的颜色、线宽、比率文字的大小和颜色、动画时长等等,核心代码如下:

/**
 * 上下文
 */
private Context mContext;

/**
 * 控件的宽高
 */
private int viewHeight, viewWidth;

/**
 * 内圆和外圆的画笔
 */
private Paint outCirclePaint, innerCirclePaint;

/**
 * 内圆和外圆的颜色
 */
private int outCircleColor, innerCircleColor;

/**
 * 内圆和外圆的画笔宽度
 */
private int outCircleStrokeWidth, innerCircleStrokeWidth;

/**
 * 比率数字画笔和其他文字的画笔
 */
private Paint ratePaint, expressPaint, percentPaint;

/**
 * 比率数字画笔和其他文字的画笔的颜色
 */
private int rateColor, expressColor, percentColor;

/**
 * 比率数字和其他文字的大小
 */
private int rateSize, expressSize, percentSize;

/**
 * 比率数字  默认45
 */
private String rateText;

/**
 * 比率代表的含义  比如 正确率、签到率等等
 */
private String expressText;

/**
 * 比率对应的弧度
 */
private float sweep;

/**
 * 动画进行的时间
 */
private int duration;

public CircleRateView(Context context) {
    super(context);
    initView(context, null, -1);
}

public CircleRateView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    initView(context, attrs, -1);
}

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

private void initView(Context context, AttributeSet attrs, int defStyleAttr) {

        ……

        //初始化相关画笔
        outCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        outCirclePaint.setStrokeWidth(dip2px(outCircleStrokeWidth));
        outCirclePaint.setStyle(Paint.Style.STROKE);
        outCirclePaint.setColor(outCircleColor);

        innerCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        innerCirclePaint.setStrokeWidth(dip2px(innerCircleStrokeWidth));
        innerCirclePaint.setStyle(Paint.Style.STROKE);
        innerCirclePaint.setColor(innerCircleColor);

        ratePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        ratePaint.setStrokeWidth(dip2px(1));
        ratePaint.setStyle(Paint.Style.FILL);
        ratePaint.setTextSize(rateSize);
        ratePaint.setTextAlign(Paint.Align.CENTER);
        ratePaint.setColor(rateColor);

        expressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        expressPaint.setStrokeWidth(dip2px(1));
        expressPaint.setStyle(Paint.Style.FILL);
        expressPaint.setTextSize(expressSize);
        expressPaint.setTextAlign(Paint.Align.CENTER);
        expressPaint.setColor(expressColor);

        percentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        percentPaint.setStrokeWidth(dip2px(1));
        percentPaint.setStyle(Paint.Style.FILL);
        percentPaint.setTextSize(percentSize);
        percentPaint.setTextAlign(Paint.Align.LEFT);
        percentPaint.setColor(percentColor);

        ……
    }

自定义View之 onMeasure

这个方法的详细介绍可以自己搜搜看,这里就不做叙述,主要是通过 MeasureSpec 这个类来测量宽高,并设置给View

核心代码如下:

 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthValue = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightValue = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode == MeasureSpec.EXACTLY) {
            viewWidth = widthValue;
        } else {
            viewWidth = dip2px(162);
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            viewHeight = heightValue;
        } else {
            viewHeight = dip2px(162);
        }

        //将View的宽高设置为正方形,使其宽高一致,等于最小的那个
        if (viewWidth <= viewHeight) {
            viewHeight = viewWidth;
        } else {
            viewWidth = viewHeight;
        }

        setMeasuredDimension(viewWidth, viewHeight);

自定义View之 onDraw

这个方法就是自定义View的核心了,文字和圆的绘制都在这个方法里。

1.绘制外圆:

外圆的半径等于控件宽的一半,中心为宽高的各一半,根据API直接画圆即可。值得一提,因为后续想要将外圆的线宽度可控制,所以圆的半径减去了线宽,至于减1dp的值,是为了美观性,外圆不至于擦边。

//画外圆
canvas.drawCircle(viewWidth / 2, viewHeight / 2, viewWidth / 2 - outCircleStrokeWidth - dip2px(1), outCirclePaint);

2.绘制内圆:

内圆是具有动画效果的,但是在动画之前,其本质就是一段圆弧,所以先根据API画一段圆弧。这里的sweep是百分比对应的弧度,后续动画的时候会用到

//画内圆
RectF rectF = new RectF(outCircleStrokeWidth + innerCircleStrokeWidth + dip2px(8), outCircleStrokeWidth + innerCircleStrokeWidth + dip2px(8), viewWidth - dip2px(8) - outCircleStrokeWidth - innerCircleStrokeWidth, viewHeight - dip2px(8) - outCircleStrokeWidth - innerCircleStrokeWidth);
canvas.drawArc(rectF, 90, sweep, false, innerCirclePaint);

3.画文字和比率:

绘制文字的时候,就涉及到绘制的中心点和基线的问题了,中心点的对其方式不同,绘制效果就不同,有时候根据位置来选择不同的对其方式,会使绘制更加的简单。关于文字的需要使用到 FontMetrics 这个相关的类,具体不做复述,不了解自己去搜下,后续我也会开一篇新章去讲一下

        //获得比率数字的FontMetricsInt
        Paint.FontMetricsInt rateFont = ratePaint.getFontMetricsInt();
        ratePaint.setTextAlign(Paint.Align.CENTER);
        int rateX = viewWidth / 2;
        int rateY = viewHeight / 2 + (rateFont.descent / 2 - rateFont.ascent / 2 - rateFont.descent);
        canvas.drawText(rateText, rateX, rateY, ratePaint);

        //获取表达意思文字的FontMetricsInt
        Paint.FontMetricsInt textFout = expressPaint.getFontMetricsInt();
        expressPaint.setTextAlign(Paint.Align.CENTER);
        int expressX = viewWidth / 2;
        int expressY = (int) (viewHeight * 0.26 + (textFout.descent / 2 - textFout.ascent / 2 - textFout.descent));
        canvas.drawText(expressText, expressX, expressY, expressPaint);

        //根据比率文字的宽度来计算百分比的x坐标,y坐标则和比率保持一致
        percentPaint.setTextAlign(Paint.Align.LEFT);
        float rateWidth = ratePaint.measureText(rateText);
        int percentX = (int) (rateWidth / 2 + viewWidth / 2 + dip2px(11));
        int percentY = viewHeight / 2 + (rateFont.descent / 2 - rateFont.ascent / 2 - rateFont.descent);
        canvas.drawText("%", percentX, percentY, percentPaint);

至此,静态的绘制过程就完成了,效果如下:

image

属性动画

前文讲到sweep是动画的弧度,这个动画的效果其实很简单,就是sweep从0到45的过程,只需要对sweep进行属性动画即可,在sweep增加的过程中不断的进行重绘就可以达到效果。

        ValueAnimator valueAnimator = new ValueAnimator().ofFloat(0, sweep);
        valueAnimator.setDuration(duration);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                sweep = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.start();

        ValueAnimator numAnimator = new ValueAnimator().ofFloat(0, Float.parseFloat(rateText));
        numAnimator.setDuration(duration);
        numAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Float rate = (Float) animation.getAnimatedValue();
                BigDecimal bigDecimal = new BigDecimal(rate);
                //四舍五入,不要小数
                bigDecimal = bigDecimal.setScale(0, BigDecimal.ROUND_HALF_UP);
                rateText = bigDecimal.toString();
                invalidate();
            }
        });
        numAnimator.start();

自定义属性

为了方便使用,自定义了一些属性,可在xml中直接使用属性进行设置。

1.定义属性

        <declare-styleable name="CircleRateView">
        <attr name="outCircleColor" format="color" />
        <attr name="innerCircleColor" format="color" />
        <attr name="rateColor" format="color" />
        <attr name="expressColor" format="color" />
        <attr name="percentColor" format="color" />
        <attr name="rateSize" format="dimension" />
        <attr name="expressSize" format="dimension" />
        <attr name="percentSize" format="dimension" />
        <attr name="outCircleStrokeWidth" format="dimension" />
        <attr name="innerCircleStrokeWidth" format="dimension" />
        <attr name="rateText" format="string" />
        <attr name="expressText" format="string" />
        <attr name="duration" format="integer" />

2.赋值属性

        //通过读取attr文件获取属性值,无配置即默认值
        TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.CircleRateView);
        outCircleColor = typedArray.getColor(R.styleable.CircleRateView_outCircleColor, Color.BLACK);
        innerCircleColor = typedArray.getColor(R.styleable.CircleRateView_innerCircleColor, Color.BLACK);
        rateColor = typedArray.getColor(R.styleable.CircleRateView_rateColor, Color.BLACK);
        expressColor = typedArray.getColor(R.styleable.CircleRateView_expressColor, Color.BLACK);
        percentColor = typedArray.getColor(R.styleable.CircleRateView_percentColor, Color.BLACK);
        rateSize = typedArray.getDimensionPixelSize(R.styleable.CircleRateView_rateSize, dip2px(50));
        expressSize = typedArray.getDimensionPixelSize(R.styleable.CircleRateView_expressSize, dip2px(16));
        percentSize = typedArray.getDimensionPixelSize(R.styleable.CircleRateView_percentSize, dip2px(16));
        outCircleStrokeWidth = typedArray.getDimensionPixelSize(R.styleable.CircleRateView_outCircleStrokeWidth, dip2px(0.5f));
        innerCircleStrokeWidth = typedArray.getDimensionPixelSize(R.styleable.CircleRateView_innerCircleStrokeWidth, dip2px(0.5f));
        rateText = typedArray.getString(R.styleable.CircleRateView_rateText);
        if (TextUtils.isEmpty(rateText)) {
            rateText = "45";
        }
        expressText = typedArray.getString(R.styleable.CircleRateView_expressText);
        if (TextUtils.isEmpty(expressText)) {
            expressText = "正确率";
        }
        duration = typedArray.getInt(R.styleable.CircleRateView_duration, 2000);
        //记得回收对象
        typedArray.recycle();

3.使用属性

 <com.feyolny.view.CircleRateView
        android:id="@+id/circleRateView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        app:duration="2000"
        app:expressColor="#BBBBBB"
        app:expressSize="18dp"
        app:expressText="错误率"
        app:innerCircleColor="#4755E8"
        app:innerCircleStrokeWidth="1dp"
        app:outCircleColor="#dbdbdb"
        app:outCircleStrokeWidth="2dp"
        app:percentColor="#BBBBBB"
        app:percentSize="18dp"
        app:rateColor="#4755E8" />

分享

本文至此结束,本着分享和重复使用的初衷,本控件源码已经上传至github,可直接在项目中添加依赖,进行使用。

源码及详细的使用方法和相关API介绍请点击:

GitHub: https://github.com/feyolny/CircleRateView

交流

我的GitHub:https://github.com/feyolny
我的简书:https://www.jianshu.com/u/79cb1fd42ea9
我的CSDN:https://blog.csdn.net/feyolny

欢迎大家一起交流!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355

推荐阅读更多精彩内容