帶你體驗Android自定義圓形刻度羅盤 儀表盤 實現指針動態改變
近期有一個自定義View的功能,類似於儀表盤的模型,可以將指針動態指定到某一個刻度上,話不多說,先上圖
先說下思路
1.先獲取自定義的一些屬性,初始化一些資源
2.在onMeasure中測量控件的具體大小
3.然后就在onDraw中先繪制有漸變色的圓弧形色帶
4.再繪制幾個大的刻度和刻度值
5.再繪制兩個大刻度之間的小刻度
6.再繪制處於正中間的圓和三角形指針
7.最后繪制實時值
其實這也從側面體現了一個自定義view的流程
1.繼承View,重寫構造方法
2.加載自定義屬性和其它資源
3.重寫onMeasure方法去確定控件的大小
4.重寫onDraw方法去繪制
5.如果有點擊事件的話,還得重寫onTouchEvent或者dispatchTouchEvent去處理點擊事件
來上代碼吧,具體注釋已經寫的很詳細了
-
public class NoiseboardView extends View {
-
-
final String TAG = "NoiseboardView";
-
-
private int mRadius; // 圓弧半徑
-
private int mBigSliceCount; // 大份數
-
private int mScaleCountInOneBigScale; // 相鄰兩個大刻度之間的小刻度個數
-
private int mScaleColor; // 刻度顏色
-
private int mScaleTextSize; // 刻度字體大小
-
private String mUnitText = ""; // 單位
-
private int mUnitTextSize; // 單位字體大小
-
private int mMinValue; // 最小值
-
private int mMaxValue; // 最大值
-
private int mRibbonWidth; // 色條寬
-
-
private int mStartAngle; // 起始角度
-
private int mSweepAngle; // 掃過角度
-
-
private int mPointerRadius; // 三角形指針半徑
-
private int mCircleRadius; // 中心圓半徑
-
-
private float mRealTimeValue = 0.0f; // 實時值
-
-
private int mBigScaleRadius; // 大刻度半徑
-
private int mSmallScaleRadius; // 小刻度半徑
-
private int mNumScaleRadius; // 數字刻度半徑
-
-
private int mViewColor_green; // 字體顏色
-
private int mViewColor_yellow; // 字體顏色
-
private int mViewColor_orange; // 字體顏色
-
private int mViewColor_red; // 字體顏色
-
-
private int mViewWidth; // 控件寬度
-
private int mViewHeight; // 控件高度
-
private float mCenterX;//中心點圓坐標x
-
private float mCenterY;//中心點圓坐標y
-
-
private Paint mPaintScale;//圓盤上大小刻度畫筆
-
private Paint mPaintScaleText;//圓盤上刻度值畫筆
-
private Paint mPaintCirclePointer;//繪制中心圓,指針
-
private Paint mPaintValue;//繪制實時值
-
private Paint mPaintRibbon;//繪制色帶
-
-
private RectF mRectRibbon;//存儲色帶的矩形數據
-
private Rect mRectScaleText;//存儲刻度值的矩形數據
-
private Path path;//繪制指針的路徑
-
-
private int mSmallScaleCount; // 小刻度總數
-
private float mBigScaleAngle; // 相鄰兩個大刻度之間的角度
-
private float mSmallScaleAngle; // 相鄰兩個小刻度之間的角度
-
-
private String[] mGraduations; // 每個大刻度的刻度值
-
private float initAngle;//指針實時角度
-
-
private SweepGradient mSweepGradient ;//設置漸變
-
private int[] color = new int[7];//漸變顏色組
-
-
public NoiseboardView(Context context) {
-
this(context, null);
-
}
-
-
public NoiseboardView(Context context, AttributeSet attrs) {
-
this(context, attrs, 0);
-
}
-
-
public NoiseboardView(Context context, AttributeSet attrs, int defStyleAttr) {
-
super(context, attrs, defStyleAttr);
-
//自定義屬性
-
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NoiseboardView, defStyleAttr, 0);
-
-
mRadius = a.getDimensionPixelSize(R.styleable.NoiseboardView_radius, dpToPx(80));
-
mBigSliceCount = a.getInteger(R.styleable.NoiseboardView_bigSliceCount, 5);
-
mScaleCountInOneBigScale = a.getInteger(R.styleable.NoiseboardView_sliceCountInOneBigSlice, 5);
-
mScaleColor = a.getColor(R.styleable.NoiseboardView_scaleColor, Color.WHITE);
-
mScaleTextSize = a.getDimensionPixelSize(R.styleable.NoiseboardView_scaleTextSize, spToPx(12));
-
mUnitText = a.getString(R.styleable.NoiseboardView_unitText);
-
mUnitTextSize = a.getDimensionPixelSize(R.styleable.NoiseboardView_unitTextSize, spToPx(14));
-
mMinValue = a.getInteger(R.styleable.NoiseboardView_minValue, 0);
-
mMaxValue = a.getInteger(R.styleable.NoiseboardView_maxValue, 150);
-
mRibbonWidth = a.getDimensionPixelSize(R.styleable.NoiseboardView_ribbonWidth, 0);
-
-
a.recycle();
-
init();
-
}
-
-
private void init() {
-
-
//起始角度是從水平正方向即(鍾表3點鍾方向)開始從0算的,掃過的角度是按順時針方向算
-
mStartAngle = 175;
-
mSweepAngle = 190;
-
-
mPointerRadius = mRadius / 3 * 2;
-
mCircleRadius = mRadius / 17;
-
-
mSmallScaleRadius = mRadius - dpToPx(10);
-
mBigScaleRadius = mRadius - dpToPx(18);
-
mNumScaleRadius = mRadius - dpToPx(20);
-
-
mSmallScaleCount = mBigSliceCount * 5;
-
mBigScaleAngle = mSweepAngle / (float) mBigSliceCount;
-
mSmallScaleAngle = mBigScaleAngle / mScaleCountInOneBigScale;
-
mGraduations = getMeasureNumbers();
-
-
//確定控件的寬度 padding值,在構造方法執行完就被賦值
-
mViewWidth = getPaddingLeft() + mRadius * 2 + getPaddingRight() + dpToPx(4);
-
mViewHeight = mViewWidth;
-
mCenterX = mViewWidth / 2.0f;
-
mCenterY = mViewHeight / 2.0f;
-
-
mPaintScale = new Paint();
-
mPaintScale.setAntiAlias(true);
-
mPaintScale.setColor(mScaleColor);
-
mPaintScale.setStyle(Paint.Style.STROKE);
-
mPaintScale.setStrokeCap(Paint.Cap.ROUND);
-
-
mPaintScaleText = new Paint();
-
mPaintScaleText.setAntiAlias(true);
-
mPaintScaleText.setColor(mScaleColor);
-
mPaintScaleText.setStyle(Paint.Style.STROKE);
-
-
mPaintCirclePointer = new Paint();
-
mPaintCirclePointer.setAntiAlias(true);
-
-
mRectScaleText = new Rect();
-
path = new Path();
-
-
mPaintValue = new Paint();
-
mPaintValue.setAntiAlias(true);
-
mPaintValue.setStyle(Paint.Style.STROKE);
-
mPaintValue.setTextAlign(Paint.Align.CENTER);
-
mPaintValue.setTextSize(mUnitTextSize);
-
-
initAngle = getAngleFromResult(mRealTimeValue);
-
-
mViewColor_green = getResources().getColor(R.color.green_value);
-
mViewColor_yellow = getResources().getColor(R.color.yellow_value);
-
mViewColor_orange = getResources().getColor(R.color.orange_value);
-
mViewColor_red = getResources().getColor(R.color.red_value);
-
color[0] = mViewColor_red;
-
color[1] = mViewColor_red;
-
color[2] = mViewColor_green;
-
color[3] = mViewColor_green;
-
color[4] = mViewColor_yellow;
-
color[5] = mViewColor_orange;
-
color[6] = mViewColor_red;
-
-
//色帶畫筆
-
mPaintRibbon = new Paint();
-
mPaintRibbon.setAntiAlias(true);
-
mPaintRibbon.setStyle(Paint.Style.STROKE);
-
mPaintRibbon.setStrokeWidth(mRibbonWidth);
-
mSweepGradient = new SweepGradient(mCenterX, mCenterY,color,null);
-
mPaintRibbon.setShader(mSweepGradient);//設置漸變 從X軸正方向取color數組顏色開始漸變
-
-
if (mRibbonWidth > 0) {
-
int r = mRadius - mRibbonWidth / 2 + dpToPx(1) ;
-
mRectRibbon = new RectF(mCenterX - r, mCenterY - r, mCenterX + r, mCenterY + r);
-
}
-
}
-
-
/**
-
* 確定每個大刻度的值
-
* @return
-
*/
-
private String[] getMeasureNumbers() {
-
String[] strings = new String[mBigSliceCount + 1];
-
for (int i = 0; i <= mBigSliceCount; i++) {
-
if (i == 0) {
-
strings[i] = String.valueOf(mMinValue);
-
} else if (i == mBigSliceCount) {
-
strings[i] = String.valueOf(mMaxValue);
-
} else {
-
strings[i] = String.valueOf(((mMaxValue - mMinValue) / mBigSliceCount) * i);
-
}
-
}
-
return strings;
-
}
-
-
/**
-
* <dt>UNSPECIFIED : 0 << 30 = 0</dt>
-
* <dd>
-
* 父控件沒有對子控件做限制,子控件可以是自己想要的尺寸
-
* 其實就是子空間在布局里沒有設置寬高,但布局里添加控件都要設置寬高,所以這種情況暫時沒碰到
-
* </dd>
-
*
-
* <dt>EXACTLY : 1 << 30 = 1073741824</dt>
-
* <dd>
-
* 父控件給子控件決定了確切大小,子控件將被限定在給定的邊界里。
-
* 如果是填充父窗體(match_parent),說明父控件已經明確知道子控件想要多大的尺寸了,也是這種模式
-
* </dd>
-
*
-
* <dt>AT_MOST : 2 << 30 = -2147483648</dt>
-
* <dd>
-
* 在布局設置wrap_content,父控件並不知道子控件到底需要多大尺寸(具體值),
-
* 需要子控件在onMeasure測量之后再讓父控件給他一個盡可能大的尺寸以便讓內容全部顯示
-
* 如果在onMeasure沒有指定控件大小,默認會填充父窗體,因為在view的onMeasure源碼中,
-
* AT_MOST(相當於wrap_content )和EXACTLY (相當於match_parent )兩種情況返回的測量寬高都是specSize,
-
* 而這個specSize正是父控件剩余的寬高,所以默認onMeasure方法中wrap_content 和match_parent 的效果是一樣的,都是填充剩余的空間。
-
* </dd>
-
*
-
* @param widthMeasureSpec
-
* @param heightMeasureSpec
-
*/
-
@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);
-
-
//在布局中設置了具體值
-
if (widthMode == MeasureSpec.EXACTLY)
-
mViewWidth = widthSize;
-
-
//在布局中設置 wrap_content,控件就取能完全展示內容的寬度(同時需要考慮屏幕的寬度)
-
if (widthMode == MeasureSpec.AT_MOST)
-
mViewWidth = Math.min(mViewWidth, widthSize);
-
-
if (heightMode == MeasureSpec.EXACTLY) {
-
mViewHeight = heightSize;
-
} else {
-
-
float[] point1 = getCoordinatePoint(mRadius, mStartAngle);
-
float[] point2 = getCoordinatePoint(mRadius, mStartAngle + mSweepAngle);
-
float maxY = Math.max(Math.abs(point1[1]) - mCenterY, Math.abs(point2[1]) - mCenterY);
-
float f = mCircleRadius + dpToPx(2) + dpToPx(25) ;
-
float max = Math.max(maxY, f);
-
mViewHeight = (int) (max + mRadius + getPaddingTop() + getPaddingBottom() + dpToPx(2) * 2);
-
-
if (heightMode == MeasureSpec.AT_MOST)
-
mViewHeight = Math.min(mViewHeight, heightSize);
-
}
-
-
//保存測量寬度和測量高度
-
setMeasuredDimension(mViewWidth, mViewHeight);
-
}
-
-
-
@Override
-
protected void onDraw(Canvas canvas) {
-
// 繪制色帶
-
canvas.drawArc(mRectRibbon, 170, 199, false, mPaintRibbon);
-
-
mPaintScale.setStrokeWidth(dpToPx(2));
-
for (int i = 0; i <= mBigSliceCount; i++) {
-
//繪制大刻度
-
float angle = i * mBigScaleAngle + mStartAngle;
-
float[] point1 = getCoordinatePoint(mRadius, angle);
-
float[] point2 = getCoordinatePoint(mBigScaleRadius, angle);
-
canvas.drawLine(point1[0], point1[1], point2[0], point2[1], mPaintScale);
-
-
//繪制圓盤上的數字
-
mPaintScaleText.setTextSize(mScaleTextSize);
-
String number = mGraduations[i];
-
mPaintScaleText.getTextBounds(number, 0, number.length(), mRectScaleText);
-
if (angle % 360 > 135 && angle % 360 < 215) {
-
mPaintScaleText.setTextAlign(Paint.Align.LEFT);
-
} else if ((angle % 360 >= 0 && angle % 360 < 45) || (angle % 360 > 325 && angle % 360 <= 360)) {
-
mPaintScaleText.setTextAlign(Paint.Align.RIGHT);
-
} else {
-
mPaintScaleText.setTextAlign(Paint.Align.CENTER);
-
}
-
float[] numberPoint = getCoordinatePoint(mNumScaleRadius, angle);
-
if (i == 0 || i == mBigSliceCount) {
-
canvas.drawText(number, numberPoint[0], numberPoint[1] + (mRectScaleText.height() / 2), mPaintScaleText);
-
} else {
-
canvas.drawText(number, numberPoint[0], numberPoint[1] + mRectScaleText.height(), mPaintScaleText);
-
}
-
}
-
-
//繪制小的子刻度
-
mPaintScale.setStrokeWidth(dpToPx(1));
-
for (int i = 0; i < mSmallScaleCount; i++) {
-
if (i % mScaleCountInOneBigScale != 0) {
-
float angle = i * mSmallScaleAngle + mStartAngle;
-
float[] point1 = getCoordinatePoint(mRadius, angle);
-
float[] point2 = getCoordinatePoint(mSmallScaleRadius, angle);
-
-
mPaintScale.setStrokeWidth(dpToPx(1));
-
canvas.drawLine(point1[0], point1[1], point2[0], point2[1], mPaintScale);
-
}
-
}
-
-
if (mRealTimeValue <= 40) {
-
mPaintValue.setColor(mViewColor_green);
-
mPaintCirclePointer.setColor(mViewColor_green);
-
} else if (mRealTimeValue > 40 && mRealTimeValue <= 90) {
-
mPaintValue.setColor(mViewColor_yellow);
-
mPaintCirclePointer.setColor(mViewColor_yellow);
-
} else if (mRealTimeValue > 90 && mRealTimeValue <= 120) {
-
mPaintValue.setColor(mViewColor_orange);
-
mPaintCirclePointer.setColor(mViewColor_orange);
-
} else {
-
mPaintValue.setColor(mViewColor_red);
-
mPaintCirclePointer.setColor(mViewColor_red);
-
}
-
-
//繪制中心點的圓
-
mPaintCirclePointer.setStyle(Paint.Style.STROKE);
-
mPaintCirclePointer.setStrokeWidth(dpToPx(4));
-
canvas.drawCircle(mCenterX, mCenterY, mCircleRadius + dpToPx(3), mPaintCirclePointer);
-
-
//繪制三角形指針
-
path.reset();
-
mPaintCirclePointer.setStyle(Paint.Style.FILL);
-
float[] point1 = getCoordinatePoint(mCircleRadius / 2, initAngle + 90);
-
path.moveTo(point1[0], point1[1]);
-
float[] point2 = getCoordinatePoint(mCircleRadius / 2, initAngle - 90);
-
path.lineTo(point2[0], point2[1]);
-
float[] point3 = getCoordinatePoint(mPointerRadius, initAngle);
-
path.lineTo(point3[0], point3[1]);
-
path.close();
-
canvas.drawPath(path, mPaintCirclePointer);
-
-
// 繪制三角形指針底部的圓弧效果
-
canvas.drawCircle((point1[0] + point2[0]) / 2, (point1[1] + point2[1]) / 2, mCircleRadius / 2, mPaintCirclePointer);
-
-
//繪制實時值
-
canvas.drawText(trimFloat(mRealTimeValue)+" "+ mUnitText, mCenterX, mCenterY - mRadius / 3 , mPaintValue);
-
}
-
-
private int dpToPx(int dp) {
-
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
-
}
-
-
private int spToPx(int sp) {
-
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
-
}
-
-
/**
-
* 依圓心坐標,半徑,扇形角度,計算出扇形終射線與圓弧交叉點的xy坐標
-
*/
-
public float[] getCoordinatePoint(int radius, float cirAngle) {
-
float[] point = new float[2];
-
-
double arcAngle = Math.toRadians(cirAngle); //將角度轉換為弧度
-
if (cirAngle < 90) {
-
point[0] = (float) (mCenterX + Math.cos(arcAngle) * radius);
-
point[1] = (float) (mCenterY + Math.sin(arcAngle) * radius);
-
} else if (cirAngle == 90) {
-
point[0] = mCenterX;
-
point[1] = mCenterY + radius;
-
} else if (cirAngle > 90 && cirAngle < 180) {
-
arcAngle = Math.PI * (180 - cirAngle) / 180.0;
-
point[0] = (float) (mCenterX - Math.cos(arcAngle) * radius);
-
point[1] = (float) (mCenterY + Math.sin(arcAngle) * radius);
-
} else if (cirAngle == 180) {
-
point[0] = mCenterX - radius;
-
point[1] = mCenterY;
-
} else if (cirAngle > 180 && cirAngle < 270) {
-
arcAngle = Math.PI * (cirAngle - 180) / 180.0;
-
point[0] = (float) (mCenterX - Math.cos(arcAngle) * radius);
-
point[1] = (float) (mCenterY - Math.sin(arcAngle) * radius);
-
} else if (cirAngle == 270) {
-
point[0] = mCenterX;
-
point[1] = mCenterY - radius;
-
} else {
-
arcAngle = Math.PI * (360 - cirAngle) / 180.0;
-
point[0] = (float) (mCenterX + Math.cos(arcAngle) * radius);
-
point[1] = (float) (mCenterY - Math.sin(arcAngle) * radius);
-
}
-
-
Log.e("getCoordinatePoint","radius="+radius+",cirAngle="+cirAngle+",point[0]="+point[0]+",point[1]="+point[1]);
-
return point;
-
}
-
-
/**
-
* 通過實時數值得到指針角度
-
*/
-
private float getAngleFromResult(float result) {
-
if (result > mMaxValue)
-
return 360.0f;
-
return mSweepAngle * (result - mMinValue) / (mMaxValue - mMinValue) + mStartAngle;
-
}
-
-
/**
-
* float類型如果小數點后為零則顯示整數否則保留
-
*/
-
public static String trimFloat(float value) {
-
if (Math.round(value) - value == 0) {
-
return String.valueOf((long) value);
-
}
-
return String.valueOf(value);
-
}
-
-
-
public float getRealTimeValue() {
-
return mRealTimeValue;
-
}
-
-
/**
-
* 實時設置讀數值
-
* @param realTimeValue
-
*/
-
public void setRealTimeValue(float realTimeValue) {
-
if (realTimeValue > mMaxValue) return;
-
mRealTimeValue = realTimeValue;
-
initAngle = getAngleFromResult(mRealTimeValue);
-
invalidate();
-
}
-
-
}
具體代碼請看Github
沒有梯子請點擊這里下載