前言: 最近一直在做h5,感觉学的东西多了还真有点混淆了,再来看anroid的时候,觉得有点点陌生了,难道真的是鱼与熊掌不可兼得吗? 好吧,也罢~ 在技术群中看到一个小伙伴有一个这样的需求,所以在不是很忙的情况下试着去实现了一下,感觉还不错!~~
先上一张最终的结果图(我这样算不算侵权啊,小伙伴产品好不容易设计出来,就被我用啦! 哈哈~~~不管了,反正android本身都是开源的):
效果还是不错的啊,不过小伙伴也不要直接放在项目中啊,还是需要调试调试的。
其实吧,总体来说还是比较简单的,也没用到什么难的东西,就是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>
好啦!!这时我们运行代码可以看到这么一个效果:
可以看到,我们最里面的弧度就画出来了。
下面我们来画我们的刻度尺(这也是这个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};
}
我们运行我们的代码:
然后我们画出我们的文字:
@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);
}
运行看到效果:
好啦,照着前面画弧线的方法,我们来画出最外面的弧线跟进度条:
@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:
好啦!!!最后动态实现进度条,然后暴露方法去设置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