Android自定义view之(刻度尺view)

前言: 最近一直在做h5,感觉学的东西多了还真有点混淆了,再来看anroid的时候,觉得有点点陌生了,难道真的是鱼与熊掌不可兼得吗? 好吧,也罢~ 在技术群中看到一个小伙伴有一个这样的需求,所以在不是很忙的情况下试着去实现了一下,感觉还不错!~~

先上一张最终的结果图(我这样算不算侵权啊,小伙伴产品好不容易设计出来,就被我用啦! 哈哈~~~不管了,反正android本身都是开源的):

Android自定义view之(刻度尺view)_第1张图片

效果还是不错的啊,不过小伙伴也不要直接放在项目中啊,还是需要调试调试的。

其实吧,总体来说还是比较简单的,也没用到什么难的东西,就是canvas的简单的drawArc、drawLine、drawtext等等,下面让我们一起来实现一下吧~~小伙伴跟紧了哦。

遇到自定义view也不要怕,至少套路得懂。

第一步: 创建一个叫MeasureView的类去继承view,然后重写构造方法。


public class MeasureView extends View {
    public MeasureView(Context context) {
        this(context, null);
    }

    public MeasureView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MeasureView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
    }
}

第二步: 做一些初始化的工作

public class MeasureView extends View {
    //弧形开始的角度
    private static final int startAngle = 180;
    //弧面所跨的弧度
    private static final int sweepAngle = 180;
    //里面数字的单位
    private static final String unint = "mmhg";
    //每隔多少画一个刻度
    private static final int angPre = 2;
    //总刻度
    private static final int totalDial = 90;
    //进度条的底色
    private static final int PROGRESS_COLOR = 0x55000000;
    //画普通的线用的笔
    private Paint linePaint;
    //画文字用的笔
    private Paint textPaint;
    //画进度条用的笔
    private Paint progressPaint;
    //里面半圆的半径
    private int innerRadius = dp2px(100);
    //最内层的padding
    private int innerPadding = dp2px(6);
    //外两层的padding
    private int outerPadding = dp2px(10);
    //进度条的宽度
    private int progressLineW = dp2px(8);
    //最里面跟最外面的线的宽度
    private int innerLineWidth = dp2px(1);
    //刻度线的宽度
    private int outerLineWidth = dp2px(2);
    //刻度线的高度
    private int outerLineHeight = dp2px(10);
    //文字的size
    private float textSize = sp2px(18);
    //单位文字的size
    private float textSizeUnit = sp2px(13);

    //当前进度
    private float currProgress = 0.5f;
    //起始值
    private float start = 0;
    //最终值
    private float end = 150;

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

    public MeasureView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MeasureView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setColor(Color.WHITE);
        progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        progressPaint.setStyle(Paint.Style.STROKE);
        progressPaint.setColor(Color.WHITE);
        progressPaint.setStrokeWidth(progressLineW);
        progressPaint.setStrokeCap(Paint.Cap.ROUND);
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        textPaint.setStyle(Paint.Style.STROKE);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextSize(textSize);
    }
  }

定义好了一些基本的变量后,我们就正式开动了(当然这里定义的这些值,一般需要定义attr使其在xml中也可以用,我就直接略过了)

第三 步:重写onMeasure方法,确定view的大小(我们这就不考虑wrapcontent的情况了,只考虑EXACTLY的值)

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //最里面圆的半径计算
        //半径=控件的宽度/2-最里层的padding-刻度线的高度-里第二层的padding-进度条的宽度-第二层padding-最外层线宽度
        innerRadius = getMeasuredWidth() / 2 - innerPadding - outerLineHeight -
                outerPadding - progressLineW - outerPadding - innerLineWidth;
        //高度=刻度线的高度+padding+最里面一层的半径+padding+进度条宽度+padding+最外层线宽度
        int height = (outerLineHeight + innerPadding + innerRadius + outerPadding + progressLineW + outerPadding + innerLineWidth);
        //重新生成高度
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

onmeasure很简单啊,你一层一层数进去然后相加就可以了。

第四步:重新onDraw方法(核心方法)

我们先从简单的入手哈,首先我们画出最里面半圆的弧线:

都知道,我们如果需要画一个弧线的话,我们需要确定弧线的矩形区域

 @Override
    protected void onDraw(Canvas canvas) {
        //画出最里面的弧线
        drawInnerLine(canvas);
        }
  private void drawInnerLine(Canvas canvas) {
        //定义一个矩形区域
        RectF rectF = new RectF();
        int width = getWidth();
        //矩形的top为刻度线的高度+padding+最外层弧线线宽度+padding+padding+进度条宽度
        //left为控件的宽度/2-半圆半径
        int top = outerLineHeight + innerPadding + innerLineWidth + outerPadding * 2 + progressLineW;
        rectF.set(width / 2 - innerRadius, top, width / 2 + innerRadius, top + innerRadius * 2);
        linePaint.setStrokeWidth(innerLineWidth);
        //弧线的起始位置为180度的位置,扫过的弧度为180
        canvas.drawArc(rectF, startAngle, sweepAngle, false, linePaint);
    }

我们来测试一下:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#a7ff0000"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <com.yasin.measuredemo.view.MeasureView
        android:id="@+id/id_measure_view"
        android:layout_marginTop="40dp"
        android:layout_marginLeft="40dp"
        android:layout_marginRight="40dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
         />
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        >
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="0"
            android:textColor="#fff"
            android:textSize="14sp"
            android:layout_centerVertical="true"
            />
        <TextView
            android:layout_centerInParent="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="最近一次测量:%1$s"
            android:textColor="#fff"
            android:textSize="15sp"
            />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="150"
            android:textColor="#fff"
            android:textSize="14sp"
            android:layout_centerVertical="true"
            android:layout_alignParentRight="true"
            />
    RelativeLayout>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="10dp"
        >
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="单次测试"
            android:background="@drawable/selector_btn"
            android:textColor="#fff"
            android:textSize="14sp"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="5dp"
            android:paddingBottom="5dp"
            android:minHeight="0dp"
            />
        <Button
            android:layout_alignParentRight="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="单次测试"
            android:background="@drawable/selector_btn"
            android:textColor="#fff"
            android:textSize="14sp"
            android:paddingLeft="10dp"
            android:paddingRight="10dp"
            android:paddingTop="5dp"
            android:paddingBottom="5dp"
            android:minHeight="0dp"
            />
    RelativeLayout>
LinearLayout>

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="false">
        <shape>
            <corners android:radius="5dp"/>
            <stroke android:color="#fff" android:width="1dp"/>
            <solid android:color="#00000000"/>
        shape>
    item>
    <item android:state_pressed="true">
        <shape>
            <solid android:color="#00000000"/>
        shape>
    item>
    <item>
        <shape>
            <corners android:radius="5dp"/>
            <stroke android:color="#fff" android:width="1dp"/>
            <solid android:color="#00000000"/>
        shape>
    item>
selector>

好啦!!这时我们运行代码可以看到这么一个效果:

Android自定义view之(刻度尺view)_第2张图片

可以看到,我们最里面的弧度就画出来了。

下面我们来画我们的刻度尺(这也是这个demo稍微比较难的地方了):

 @Override
    protected void onDraw(Canvas canvas) {
        //画出最里面的弧线
        drawInnerLine(canvas);
        drawDial(startAngle, sweepAngle, totalDial, angPre, outerLineHeight, outerLineHeight / 2, innerRadius + innerPadding + outerLineHeight, canvas);
        }
  /**
     * 画刻度盘
     */
    private void drawDial(int startAngle, int allAngle, int dialCount, int per, int longLength, int shortLength, int radius, Canvas canvas) {
        linePaint.setStrokeWidth(outerLineWidth);
        int length;
        int angle;
        //根据需要显示的刻度总个数遍历
        for (int i = 0; i <= dialCount; i++) {
            //每一个刻度对应的起始角度为180度+(总度数/个数)*对应刻度的位置
            angle = (int) ((allAngle) / (dialCount * 1f) * i) + startAngle;
            //线条的起始点位置
            int[] startP;
            //线条的end点的位置
            int[] endP;
            //当i%per==0,每一个需要显示短刻度的时候(因为设计稿第一个为短的刻度条)
            if (i % per == 0) {
                //短刻度条的长度为长刻度条的一半
                length = shortLength;
                //获取刻度条起始点位置
                startP = getPointFromAngleAndRadius(angle, radius - length);
                endP = getPointFromAngleAndRadius(angle, radius - length * 2);
            } else {
                length = longLength;
                startP = getPointFromAngleAndRadius(angle, radius);
                endP = getPointFromAngleAndRadius(angle, radius - length);
            }
            //画出对应的刻度条
            canvas.drawLine(startP[0], startP[1], endP[0], endP[1], linePaint);
        }
    }

    /**
     * 根据刻度条相应的角度算出点位置
     * @param angle
     * @param radius
     * @return
     */
    private int[] getPointFromAngleAndRadius(int angle, int radius) {
        //根据三角函数公式可以知道,横坐标值为(刻度条+innnerradius)也就是刻度条对应圆的半径
        //乘以一个cos(angle),因为我们是以(getWidth() / 2,控件的高度)位置建的坐标系
        //而真正的坐标系的位置为控件左上角,所以算出的值后需要+getWidth() / 2或者getHeight()
        double x = radius * Math.cos(angle * Math.PI / 180) + getWidth() / 2;
        double y = radius * Math.sin(angle * Math.PI / 180) + getHeight();
        return new int[]{(int) x, (int) y};
    }

我们运行我们的代码:

Android自定义view之(刻度尺view)_第3张图片

然后我们画出我们的文字:


  @Override
    protected void onDraw(Canvas canvas) {
        //画出最里面的弧线
        drawInnerLine(canvas);
        drawDial(startAngle, sweepAngle, totalDial, angPre, outerLineHeight, outerLineHeight / 2, innerRadius + innerPadding + outerLineHeight, canvas);
        drawText(canvas);
        }
 private void drawText(Canvas canvas) {
        //当前文字对应的值为(0+(150-0)*当前进度)
        String currText = String.valueOf((int) (start + (end - start) * currProgress));
        //因为数字字体大而单位数字小
        textPaint.setTextSize(textSize);
        //测量数字文字对应的长度
        float numWidth = textPaint.measureText("" + currText);
        //重新设置笔的size
        textPaint.setTextSize(textSizeUnit);
        //为了获取单位文字的高度
        Rect rect = new Rect();
        //获取单位文字的最小矩形范围
        textPaint.getTextBounds(unint, 0, unint.length(), rect);
        //单位文字的宽度
        float unitWidth = textPaint.measureText(unint);
        //从新设置笔的大小
        textPaint.setTextSize(textSize);
        //文字的basex为(控件的宽度/2-(数字文字的长度+单位文字的长度)/2)
        float baseX = getWidth() / 2 - (numWidth + unitWidth) / 2;
        //文字的centery为(最外层线的宽度+padding+进度条宽度+padding+padding+最里面半圆半径的一半)
        float centerY = innerLineWidth + outerPadding + progressLineW + outerPadding + outerLineHeight + innerPadding + innerRadius / 2;
        //(主要解决文字在半圆的中心文字)根据centery算出文字的basey
        float baseY = centerY - (textPaint.ascent() + textPaint.descent()) / 2;
        //设置数字文字为粗体
        textPaint.setFakeBoldText(true);
        //画出数字文字
        canvas.drawText(currText + "", baseX, baseY, textPaint);
        //重新设置画笔
        textPaint.setTextSize(textSizeUnit);
        textPaint.setFakeBoldText(false);
        //画出单位文字(跟数字文字底部有一个偏移量所以basey-了一个(单位文字的高度的1/6))
        canvas.drawText(unint, baseX + numWidth + dp2px(1), baseY - rect.height() / 6, textPaint);
    }

运行看到效果:

Android自定义view之(刻度尺view)_第4张图片

好啦,照着前面画弧线的方法,我们来画出最外面的弧线跟进度条:

@Override
    protected void onDraw(Canvas canvas) {
        //画出最里面的弧线
        drawInnerLine(canvas);
        drawDial(startAngle, sweepAngle, totalDial, angPre, outerLineHeight, outerLineHeight / 2, innerRadius + innerPadding + outerLineHeight, canvas);
        drawText(canvas);
        drawOuterStaticLine(canvas);
        }
 private void drawOuterStaticLine(Canvas canvas) {
        //最外层的弧线
        RectF rectF1 = new RectF();
        int width = getWidth();
        rectF1.set(innerLineWidth, innerLineWidth, width - innerLineWidth, getHeight() * 2 - innerLineWidth);
        linePaint.setStrokeWidth(innerLineWidth);
        canvas.drawArc(rectF1, startAngle, sweepAngle, false, linePaint);
        //静态的进度条
        progressPaint.setColor(PROGRESS_COLOR);
        RectF rectF2 = new RectF();
        rectF2.set(innerLineWidth + outerPadding + outerPadding / 2, innerLineWidth + outerPadding + outerPadding / 2,
                width - (innerLineWidth + outerPadding + outerPadding / 2), getHeight() * 2 - ((innerLineWidth + outerPadding + outerPadding / 2)));
        canvas.drawArc(rectF2, startAngle, sweepAngle, false, progressPaint);

    }

因为原理跟我们最初画最里面的弧度的原理一样,我就不详解了,唯一难点就是多了一个padding值,所以计算好矩形区域就可以了,起始角度跟扫过弧度都是180,我们可以看到如下效果L:

Android自定义view之(刻度尺view)_第5张图片

好啦!!!最后动态实现进度条,然后暴露方法去设置progress就可以了,控制进度条进度无非就是控制弧线扫过的角度(startangle+(endangle-startangle)*progress)


    @Override
    protected void onDraw(Canvas canvas) {
        //画出最里面的弧线
        drawInnerLine(canvas);
        drawDial(startAngle, sweepAngle, totalDial, angPre, outerLineHeight, outerLineHeight / 2, innerRadius + innerPadding + outerLineHeight, canvas);
        drawText(canvas);
        drawOuterStaticLine(canvas);
        drawProgress(canvas);
    }
 /**
     * 画进度条
     *
     * @param canvas
     */
    private void drawProgress(Canvas canvas) {
        int width = getWidth();
        //设置画笔颜色为白色
        progressPaint.setColor(Color.WHITE);
        //确定矩形范围
        RectF rectF2 = new RectF();
        rectF2.set(innerLineWidth + outerPadding + outerPadding / 2, innerLineWidth + outerPadding + outerPadding / 2,
                width - (innerLineWidth + outerPadding + outerPadding / 2), getHeight() * 2 - ((innerLineWidth + outerPadding + outerPadding / 2)));
        //画出弧度 弧线扫过的角度(startangle+(endangle-startangle)*progress)
        canvas.drawArc(rectF2, startAngle, sweepAngle * currProgress, false, progressPaint);
    }

最后就是暴露设置进度的方法了:

  public void setProgress(float progress) {
        if (currProgress == progress) {
            return;
        }
        this.currProgress = progress;
        if (Looper.myLooper() == Looper.getMainLooper()) {
            invalidate();
        } else {
            postInvalidate();
        }
    }

然后就是activity中测试了:

package com.yasin.measuredemo;

import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.animation.AccelerateDecelerateInterpolator;

import com.yasin.measuredemo.view.MeasureView;

public class MainActivity extends AppCompatActivity {
    private MeasureView mMeasureView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mMeasureView= (MeasureView) findViewById(R.id.id_measure_view);
        startAni();
    }

    private void startAni() {
        ObjectAnimator a=ObjectAnimator.ofFloat(mMeasureView,"progress",0f,1f);
        a.setInterpolator(new AccelerateDecelerateInterpolator());
        a.setDuration(3000);
        a.setRepeatCount(ValueAnimator.INFINITE);
        a.setRepeatMode(ValueAnimator.REVERSE);
        a.start();
    }
}

最后看到的就是我们最初的效果图了。。。

是不是很简单呢?所以说自定义也不要怕哈,不会的话先实现简单的功能。

小伙伴如果有什么不懂的可以进群联系我哈,进群后有需求的话,我一般有空都会帮着实现的,哈哈~~请叫我红领巾!!

最后附上demo的github链接:
https://github.com/913453448/MeasureDemo

你可能感兴趣的:(Android资源)