仪表盘进度条效果(高度自定义)

可以自定义的属性:

  • 1.仪表盘半径
  • 2.仪表盘宽度
  • 3.指针大小
  • 4.刻度的密度
  • 5.可触发触摸事件对应设置进度(可选择)
  • 6.进度动画(可选择)

GitHub地址https://github.com/sdfdzx/DriverProgress

How to:

<com.xuan.driverprogress.view.DriverProgress
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        custom:indicator_radius="10dp"
        custom:panel_density="10"
        custom:panel_max="100"
        custom:panel_point_radius="3dp"
        custom:panel_progress="50"
        custom:panel_radius="75dp"
        custom:panel_width="45dp"
        custom:touchable="true"
        />

Step 1. Add the JitPack repository to your build file
Add it in your root build.gradle at the end of repositories:

    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }

Step 2. Add the dependency

dependencies {
            compile 'com.github.sdfdzx:DriverProgress:v1.0.4'
    }

前言

自定义View,最近毕设中要用一个进度条,刚好在UI中国中的一个Demo中发现了这个设计,就把他实现了。

目标

目标

实现效果

实现效果

实现思路:

1.仪表盘 = 底盘 + 进度条 + 刻度
底盘:灰色的180度圆弧

进度条:蓝色的圆弧(圆弧度数根据进度的百分比)

刻度:两种实现方式:1.同样是绘制圆弧,但是用的是虚线(效果不理想,放弃)2.利用for循环,绘制一组圆点(半径很小的圆),对应圆心的x,y坐标利用Math.cos和Math.sin计算。
2.指针 = 圆 + 圆点 + 三角形
:灰色的圆->指针的轴

圆点:白色的圆点(半径很小的圆)->指针中的圆点

指针:三角形->三角形三个顶点的坐标要随进度不停的变换计算

附加功能:

1.动画效果:利用ValueAnimator+OvershootInterpolator实现动画
2.触摸更新进度:重写setOnTouchListener,获得触摸点的x,y,利用反三角函数计算出角度,重绘。

实现思路还是比较容易的,接下来就是代码的实现了。

关键代码:

1.仪表盘绘制
(1)底盘:

this.paint.setColor(getResources().getColor(R.color.panel_bottom_white));
this.paint.setAntiAlias(true); //消除锯齿
this.paint.setStyle(Paint.Style.STROKE); //绘制空心圆
        /**
         * 绘制仪表盘底色
         */

float x = PANEL_WIDTH / 2;
float y = PANEL_WIDTH / 2;
RectF oval = new RectF(x, y,
            PANEL_WIDTH + 2 * PANEL_RADIUS, 2 * PANEL_RADIUS + PANEL_WIDTH);

this.paint.setStrokeWidth(PANEL_WIDTH);
canvas.drawArc(oval, 180, 180, false, paint);

前三行是画笔paint的属性配置,分别设置了颜色,消除锯齿,绘制空心圆
下面是主要的逻辑,这里是绘制圆弧,有几个关键点需要注意:
1.画笔的中心在线的中心!!!
2.圆弧的绘制是你设置的矩形的内切弧。
明白了这两点


这幅图应该就很好理解了
x=矩形的left=panel的width/2
y=矩形的top=panel的width/2
矩形的right = panel的width/2+panel的半径+panel的width/2
=panel的width+panel的半径
矩形的bottom=panel的半径*2(圆弧是矩形的内切弧,所以是整个圆,这里是为了说明才画了半个矩形,其实是忘了...)+panel的width(原理同right)

canvas.drawArc(oval, 180, 180, false, paint);这句话有一个地方要注意参数含义,上源码:

* @param oval       The bounds of oval used to define the shape and size
*                   of the arc
* @param startAngle Starting angle (in degrees) where the arc begins
* @param sweepAngle Sweep angle (in degrees) measured clockwise
* @param useCenter  If true, include the center of the oval in the arc, and
          close it if it is being stroked. This will draw a wedge
* @param paint      The paint used to draw the arc
     */
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, 
boolean useCenter,@NonNull Paint paint) {
        drawArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, 
                sweepAngle, useCenter,paint);
    }

所以是从一个度数开始绘制,绘制多少度数
这里有个知识点,原来的一篇文章中提到过仿QQ常驻底部栏播放按钮,就是绘制圆弧的时候,Android默认的是从3点钟方向开始绘制,所以我们要绘制如图所示的圆弧,肯定是从180度开始,绘制180度。

(2)进度条

  /**
     * 绘制仪表盘进度
     */       
    this.paint.setColor(getResources()
    .getColor(R.color.panel_progress_blue));
    this.paint.setAntiAlias(true); //消除锯齿
    this.paint.setStyle(Paint.Style.STROKE); //绘制空心圆
    this.paint.setStrokeWidth(PANEL_WIDTH);
    int sweepAngle = (int) (PANEL_PROGRESS / PANEL_MAX * 180.0f);
    canvas.drawArc(oval, 180, sweepAngle, false, paint);

上面那个绘制明白后,进度条就简单了,矩形用的事同一个矩形,唯一不同的就是绘制度数

 int sweepAngle = (int) (PANEL_PROGRESS / PANEL_MAX * 180.0f);

代码很好理解,就是根据进度计算角度比例,唯一注意的是类型转换

(3)刻度

   /**
     * 绘制仪表盘刻度
     */
    paint.setColor(getResources().getColor(R.color.white));
    paint.setStyle(Paint.Style.FILL);
    paint.setStrokeWidth(PANEL_POINT_RADIUS);

    float startx = x;
    float starty = PANEL_RADIUS + PANEL_WIDTH * 0.75f - PANEL_POINT_RADIUS;

    float radius = PANEL_RADIUS + PANEL_WIDTH / 4;
    //点的密度
    int density = 180 / PANEL_DENSITY;
    float potsx[] = new float[PANEL_DENSITY];
    float potsy[] = new float[PANEL_DENSITY];
    for (int i = 1; i < PANEL_DENSITY; i++) {
        potsx[i] = (float) (startx + (radius * (1 - Math.cos(density * i *
                    Math.PI / 180))));
        potsy[i] = (float) (starty - radius * Math.sin(density * i* 
                    Math.PI / 180) + PANEL_POINT_RADIUS);
        canvas.drawCircle(potsx[i], potsy[i], PANEL_POINT_RADIUS, paint);
    }

绘制刻度的原理上面已经说了,就是绘制一组小圆点。
首先计算起始点的坐标

float startx = x;
float starty = PANEL_RADIUS + PANEL_WIDTH * 0.75f -           
PANEL_POINT_RADIUS;    


x坐标不用解释了,这里y坐标要说明一下:不要问我为什么是这样得来的,因为我也不知道,我是试出来的......
讲道理:
starty=仪表盘半径+仪表盘宽度/2+刻度点半径(画笔在线宽的中点)
但是我按照这个逻辑来就不讲道理了......没办法,我就试,二分法思想吧...
最后得出上面的逻辑:
starty=仪表盘半径+仪表盘宽度3/4+刻度点半径*

后面就是一组点的x,y坐标了,用到一点数学知识,根据图可以看到
x=startx+(radius-radiuscosA)(坐标原点在左上角)
y=starty-radius
sinA
对应的角度A = density * i * Math.PI / 180(density表示点的密度,也就是个数)
但是这里也有个坑,那就是radius的计算,和上面一样,不要问我为什么是这样计算的,因为我也不知道,我是试出来的...
讲道理:
radius = 仪表盘半径+仪表盘宽度/2
然而最后的实现是
float radius = PANEL_RADIUS + PANEL_WIDTH / 4;
最后一个注意的点事,我是从index=1开始绘制的,不然效果不好
到此为止仪表盘绘制结束了!

2.指针绘制
(1)圆:

/**
 * 绘制底圆
 */
 this.paint.setColor(getResources().getColor(R.color.panel_indicator_color));
 //圆点
 float ix = PANEL_RADIUS + PANEL_WIDTH * 0.75f;
 float iy = PANEL_RADIUS + PANEL_WIDTH * 0.75f - PANEL_POINT_RADIUS;
        canvas.drawCircle(ix, iy, INDICATOR_RADIUS, paint);

圆应该在仪表盘圆的圆心处,但是这里又出现了1/4的问题,同上...形成了定律,反而简单了
圆心x=仪表盘半径+仪表盘宽度
圆心y=仪表盘半径+仪表盘宽度-圆的半径(画笔在中点)
最后的实现:

//圆点

float ix = PANEL_RADIUS + PANEL_WIDTH * 0.75f;
float iy = PANEL_RADIUS + PANEL_WIDTH * 0.75f - PANEL_POINT_RADIUS;

(2)白色的圆点

    /**
     * 绘制轴  白色的点
     */
 this.paint.setColor(getResources().getColor(R.color.white));
 canvas.drawCircle(ix, iy, INDICATOR_RADIUS / 3, paint);

就是把上面圆的半径/3
(3)指针(三角形)

/**
* 绘制针头  三角形
*/
float angle = (float) (PANEL_PROGRESS / PANEL_MAX * Math.PI);
float px = (float) (ix - (4 * INDICATOR_RADIUS * Math.cos(angle)));
float py = (float) (iy - (4 * INDICATOR_RADIUS * Math.sin(angle)));
Path path = new Path();

path.moveTo((float) (ix - INDICATOR_RADIUS * Math.sin(angle)),
(float) ((iy + INDICATOR_RADIUS * Math.cos(angle))));// 此点为多边形的起点

path.lineTo((float) (ix + INDICATOR_RADIUS * Math.sin(angle)),
(float) ((iy - INDICATOR_RADIUS * Math.cos(angle))));

path.lineTo(px, py);
path.close(); // 使这些点构成封闭的多边形
canvas.drawPath(path, paint);

这里用到点数学知识,最后可以得到:
角A=角B
这个结论得到后就可以计算三角形三个点的坐标了

path.moveTo((float) (ix - INDICATOR_RADIUS * Math.sin(angle)),
(float) ((iy + INDICATOR_RADIUS * Math.cos(angle))));// 此点为多边形的起点

这个就是三角形的角1

path.lineTo((float) (ix + INDICATOR_RADIUS * Math.sin(angle)),
(float) ((iy - INDICATOR_RADIUS * Math.cos(angle))));

这个就是三角形的角2
path.lineTo(px, py);
这个就是三角形的角3
最后利用Path进行绘制

-----------------------------------分割线------------------------------------------
至此为止OnDraw方法分析结束!

onMeasure方法
自定义组件不能没有onMeasure方法,对应的三种模式EXACTLY,AT_MOST,UNSPECIFIED

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            width = (int) (getPaddingLeft() + 2 * PANEL_RADIUS + 1.5f* 
                          PANEL_WIDTH + getPaddingRight());
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            height = (int) (getPaddingTop() + PANEL_RADIUS + 
                          PANEL_WIDTH + getPaddingBottom());
        }
        setMeasuredDimension(width, height);
    }

附加功能
1.动画

/**
     * 启动动画
     */
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public void startAnimation() {
        ValueAnimator animator = ValueAnimator.ofFloat(0, PANEL_PROGRESS);
        animator.setDuration(3000);

        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float curValue = (float) animation.getAnimatedValue();
                setProgress(curValue);
            }
        });
        animator.setInterpolator(new OvershootInterpolator());
        animator.start();
    }

这里用的是ValueAnimator,从0~progress,最后利用OvershootInterpolator实现了一个到达顶头后回退一点

2.触摸更改进度

setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                //圆心
                float ix = PANEL_RADIUS + PANEL_WIDTH * 0.75f;
                float iy = PANEL_RADIUS + PANEL_WIDTH * 0.75f
                               -  PANEL_POINT_RADIUS;
                float x = event.getX();
                float y = event.getY();

                float yh = iy - y;//角对边
                float progress = 0;
                //sin
                float sin = yh / (PANEL_RADIUS + PANEL_WIDTH);
                 //sin = 角对边 / 半径
                if (x < ix) {
                //度数<90 Math.asin 反三角函数
                    progress = (float) ((float) Math.asin(sin) * 2 / Math.PI
                                       * PANEL_MAX);
                } else { //度数>90
                    progress = (float) (PANEL_MAX - (float) Math.asin(sin) 
                                      * 2 / Math.PI * PANEL_MAX);
                }

                setProgress(progress);
                return true;
            }
        });
这里写图片描述

这里主要用来反三角函数
如图,假设触摸的位置是红色区域,那么只要算出对应三角形的角度,在映射成进度,设置进度即可。
计算的逻辑还是很好理解的,经历了上面的计算,这个应该很容易理解
实现触摸事件响应->触摸坐标->对应角度->对应进度->setProgress

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

推荐阅读更多精彩内容