自定义View----仿仪表盘的水柱进度条(高度定制化,已通过JitPack发布)

仪表盘进度条

—–已通过JitPack发布
可以自定义的属性:
1.仪表盘半径
2.仪表盘宽度
3.指针大小
4.刻度的密度
5.可触发触摸事件对应设置进度(可选择)
6.进度动画(可选择)
自定义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-radius*cosA)(坐标原点在左上角)
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

最后附上GitHub地址https://github.com/sdfdzx/DriverProgress

How to:

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'
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值