package com.xw.sample.dashboardviewdemo; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; /** * DashboardView style 1 * Created by woxingxiao on 2016-11-19. */ public class DashboardView1 extends View { private int mRadius; // 扇形半径 private int mStartAngle = 180; // 起始角度 private int mSweepAngle = 180; // 绘制角度 private int mMin = 0; // 最小值 private int mMax = 100; // 最大值 private int mSection = 10; // 值域(mMax-mMin)等分份数 private int mPortion = 10; // 一个mSection等分份数 private String mHeaderText = "℃"; // 表头 private int mRealTimeValue = mMin; // 实时读数 private boolean isShowValue = true; // 是否显示实时读数 private int mStrokeWidth; // 画笔宽度 private int mLength1; // 长刻度的相对圆弧的长度 private int mLength2; // 刻度读数顶部的相对圆弧的长度 private int mPLRadius; // 指针长半径 private int mPSRadius; // 指针短半径 private int mPadding; private float mCenterX, mCenterY; // 圆心坐标 private Paint mPaint; private RectF mRectFArc; private Path mPath; private RectF mRectFInnerArc; private Rect mRectText; private String[] mTexts; public DashboardView1(Context context) { this(context, null); } public DashboardView1(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DashboardView1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mStrokeWidth = dp2px(1); mLength1 = dp2px(8) + mStrokeWidth; mLength2 = mLength1 + dp2px(2); mPSRadius = dp2px(10); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStrokeCap(Paint.Cap.ROUND); mRectFArc = new RectF(); mPath = new Path(); mRectFInnerArc = new RectF(); mRectText = new Rect(); mTexts = new String[mSection + 1]; // 需要显示mSection + 1个刻度读数 for (int i = 0; i < mTexts.length; i++) { int n = (mMax - mMin) / mSection; mTexts[i] = String.valueOf(mMin + i * n); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mPadding = Math.max( Math.max(getPaddingLeft(), getPaddingTop()), Math.max(getPaddingRight(), getPaddingBottom()) ); setPadding(mPadding, mPadding, mPadding, mPadding); int width = resolveSize(dp2px(200), widthMeasureSpec); mRadius = (width - mPadding * 2 - mStrokeWidth * 2) / 2; mPaint.setTextSize(sp2px(16)); if (isShowValue) { // 显示实时读数,View高度增加字体高度3倍 mPaint.getTextBounds("0", 0, "0".length(), mRectText); } else { mPaint.getTextBounds("0", 0, 0, mRectText); } // 由半径+指针短半径+实时读数文字高度确定的高度 int height1 = mRadius + mStrokeWidth * 2 + mPSRadius + mRectText.height() * 3; // 由起始角度确定的高度 float[] point1 = getCoordinatePoint(mRadius, mStartAngle); // 由结束角度确定的高度 float[] point2 = getCoordinatePoint(mRadius, mStartAngle + mSweepAngle); // 取最大值 int max = (int) Math.max( height1, Math.max(point1[1] + mRadius + mStrokeWidth * 2, point2[1] + mRadius + mStrokeWidth * 2) ); setMeasuredDimension(width, max + getPaddingTop() + getPaddingBottom()); mCenterX = mCenterY = getMeasuredWidth() / 2f; mRectFArc.set( getPaddingLeft() + mStrokeWidth, getPaddingTop() + mStrokeWidth, getMeasuredWidth() - getPaddingRight() - mStrokeWidth, getMeasuredWidth() - getPaddingBottom() - mStrokeWidth ); mPaint.setTextSize(sp2px(10)); mPaint.getTextBounds("0", 0, "0".length(), mRectText); mRectFInnerArc.set( getPaddingLeft() + mLength2 + mRectText.height(), getPaddingTop() + mLength2 + mRectText.height(), getMeasuredWidth() - getPaddingRight() - mLength2 - mRectText.height(), getMeasuredWidth() - getPaddingBottom() - mLength2 - mRectText.height() ); mPLRadius = mRadius - (mLength2 + mRectText.height() + dp2px(5)); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); /** * 画圆弧 */ mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(mStrokeWidth); mPaint.setColor(ContextCompat.getColor(getContext(), R.color.colorPrimary)); canvas.drawArc(mRectFArc, mStartAngle, mSweepAngle, false, mPaint); /** * 画长刻度 * 画好起始角度的一条刻度后通过canvas绕着原点旋转来画剩下的长刻度 */ double cos = Math.cos(Math.toRadians(mStartAngle - 180)); double sin = Math.sin(Math.toRadians(mStartAngle - 180)); float x0 = (float) (mPadding + mStrokeWidth + mRadius * (1 - cos)); float y0 = (float) (mPadding + mStrokeWidth + mRadius * (1 - sin)); float x1 = (float) (mPadding + mStrokeWidth + mRadius - (mRadius - mLength1) * cos); float y1 = (float) (mPadding + mStrokeWidth + mRadius - (mRadius - mLength1) * sin); canvas.save(); canvas.drawLine(x0, y0, x1, y1, mPaint); float angle = mSweepAngle * 1f / mSection; for (int i = 0; i < mSection; i++) { canvas.rotate(angle, mCenterX, mCenterY); canvas.drawLine(x0, y0, x1, y1, mPaint); } canvas.restore(); /** * 画短刻度 * 同样采用canvas的旋转原理 */ canvas.save(); mPaint.setStrokeWidth(1); float x2 = (float) (mPadding + mStrokeWidth + mRadius - (mRadius - mLength1 / 2f) * cos); float y2 = (float) (mPadding + mStrokeWidth + mRadius - (mRadius - mLength1 / 2f) * sin); canvas.drawLine(x0, y0, x2, y2, mPaint); angle = mSweepAngle * 1f / (mSection * mPortion); for (int i = 1; i < mSection * mPortion; i++) { canvas.rotate(angle, mCenterX, mCenterY); if (i % mPortion == 0) { // 避免与长刻度画重合 continue; } canvas.drawLine(x0, y0, x2, y2, mPaint); } canvas.restore(); /** * 画长刻度读数 * 添加一个圆弧path,文字沿着path绘制 */ mPaint.setTextSize(sp2px(10)); mPaint.setTextAlign(Paint.Align.LEFT); mPaint.setStyle(Paint.Style.FILL); for (int i = 0; i < mTexts.length; i++) { mPaint.getTextBounds(mTexts[i], 0, mTexts[i].length(), mRectText); // 粗略把文字的宽度视为圆心角2*θ对应的弧长,利用弧长公式得到θ,下面用于修正角度 float θ = (float) (180 * mRectText.width() / 2 / (Math.PI * (mRadius - mLength2 - mRectText.height()))); mPath.reset(); mPath.addArc( mRectFInnerArc, mStartAngle + i * (mSweepAngle / mSection) - θ, // 正起始角度减去θ使文字居中对准长刻度 mSweepAngle ); canvas.drawTextOnPath(mTexts[i], mPath, 0, 0, mPaint); } /** * 画表头 * 没有表头就不画 */ if (!TextUtils.isEmpty(mHeaderText)) { mPaint.setTextSize(sp2px(14)); mPaint.setTextAlign(Paint.Align.CENTER); mPaint.getTextBounds(mHeaderText, 0, mHeaderText.length(), mRectText); canvas.drawText(mHeaderText, mCenterX, mCenterY / 2f + mRectText.height(), mPaint); } /** * 画指针 */ float θ = mStartAngle + mSweepAngle * (mRealTimeValue - mMin) / (mMax - mMin); // 指针与水平线夹角 int d = dp2px(5); // 指针由两个等腰三角形构成,d为共底边长的一半 mPath.reset(); float[] p1 = getCoordinatePoint(d, θ - 90); mPath.moveTo(p1[0], p1[1]); float[] p2 = getCoordinatePoint(mPLRadius, θ); mPath.lineTo(p2[0], p2[1]); float[] p3 = getCoordinatePoint(d, θ + 90); mPath.lineTo(p3[0], p3[1]); float[] p4 = getCoordinatePoint(mPSRadius, θ - 180); mPath.lineTo(p4[0], p4[1]); mPath.close(); canvas.drawPath(mPath, mPaint); /** * 画指针围绕的镂空圆心 */ mPaint.setColor(Color.WHITE); canvas.drawCircle(mCenterX, mCenterY, dp2px(2), mPaint); /** * 画实时度数值 */ if (isShowValue) { mPaint.setTextSize(sp2px(16)); mPaint.setTextAlign(Paint.Align.CENTER); mPaint.setColor(ContextCompat.getColor(getContext(), R.color.colorPrimary)); String value = String.valueOf(mRealTimeValue); mPaint.getTextBounds(value, 0, value.length(), mRectText); canvas.drawText(value, mCenterX, mCenterY + mPSRadius + mRectText.height() * 2, mPaint); } } private int dp2px(int dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics()); } private int sp2px(int sp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, Resources.getSystem().getDisplayMetrics()); } public float[] getCoordinatePoint(int radius, float angle) { float[] point = new float[2]; double arcAngle = Math.toRadians(angle); //将角度转换为弧度 if (angle < 90) { point[0] = (float) (mCenterX + Math.cos(arcAngle) * radius); point[1] = (float) (mCenterY + Math.sin(arcAngle) * radius); } else if (angle == 90) { point[0] = mCenterX; point[1] = mCenterY + radius; } else if (angle > 90 && angle < 180) { arcAngle = Math.PI * (180 - angle) / 180.0; point[0] = (float) (mCenterX - Math.cos(arcAngle) * radius); point[1] = (float) (mCenterY + Math.sin(arcAngle) * radius); } else if (angle == 180) { point[0] = mCenterX - radius; point[1] = mCenterY; } else if (angle > 180 && angle < 270) { arcAngle = Math.PI * (angle - 180) / 180.0; point[0] = (float) (mCenterX - Math.cos(arcAngle) * radius); point[1] = (float) (mCenterY - Math.sin(arcAngle) * radius); } else if (angle == 270) { point[0] = mCenterX; point[1] = mCenterY - radius; } else { arcAngle = Math.PI * (360 - angle) / 180.0; point[0] = (float) (mCenterX + Math.cos(arcAngle) * radius); point[1] = (float) (mCenterY - Math.sin(arcAngle) * radius); } return point; } public int getRealTimeValue() { return mRealTimeValue; } public void setRealTimeValue(int realTimeValue) { if (mRealTimeValue == realTimeValue || realTimeValue < mMin || realTimeValue > mMax) { return; } mRealTimeValue = realTimeValue; postInvalidate(); } }