android 自定義View 儀表盤 DashboardView 的實現
阿新 • • 發佈:2019-01-23
有天上班,老闆突然扔給我一張圖,
說:這個東西能不能做一下。
我說應該可以。然後老闆那就沒有下文了,我想既然問了,那我就抽空做一下。
當我做出來的時候去找老闆,我說上次你給我發的那個圖,我已經做出來了,您要不要看一下。
老闆說,不用了,不需要了。
不需要了。。 不需要了 。。 不需要了!!
聽到這句話我的內心是幾乎是崩潰的,哭哭。
好吧,既然這樣,那麼就開源出來吧(github地址),並且寫下了這篇部落格作為實現過程的記錄,也希望能給一部分人帶來一些幫助。
另:本人可能姿勢水平不太高,如果有什麼錯誤還請大家幫忙指正 , 蟹蟹。
好了 , 進入正題
首先放一張實現完成的gif
第一步,我們先在attrs檔案下新增我們的自定義屬性:
<declare-styleable name="DashboardView">
<attr name="arcColor" format="color"/>
<attr name="padding" format="dimension"/>
<attr name="android:text"/>
<attr name="tikeCount" format="integer"/>
<attr name="Unit" format="string"/>
<attr name="android:textSize"/>
<attr name="backgroundColor" format="color" />
<attr name="textColor" format="color"/>
<attr name="startProgressColor" format="color" />
<attr name="endProgressColor" format ="color" />
<attr name="startNumber" format="integer" />
<attr name="maxNumber" format="integer" />
<attr name="progressColor" format="color"/>
</declare-styleable>
第二部,新建一個java類,來管理這些屬性
public class DashboardViewAttr {
private int mTikeCount;
...
public DashboardViewAttr(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.DashboardView, defStyleAttr, 0);
mTikeCount = ta.getInt(R.styleable.DashboardView_tikeCount, 48);
...
ta.recycle();
}
public int getmTikeCount() {
return mTikeCount;
}
...
}
注意,當使用 TypedArray
進行載入屬性的時候,最後記得要回收一下,即呼叫TypedArray.recycle()
。
最後,建立我們的DashboardView
,使之繼承View
首先,實現繼承自View的三個構造方法,並且新增初始化方法,在帶有三個引數的構造方法下面例項化我們剛剛建立的屬性管理類。
public DashboardView(Context context) {
this(context, null);
init(context);
}
public DashboardView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
init(context);
}
public DashboardView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
dashboardViewattr = new DashboardViewAttr(context, attrs, defStyleAttr);
init(context);
}
init方法中又分成兩個方法,1、初始化自定義屬性,2、初始化各個畫筆:
//初始化自定義屬性
mTikeCount = dashboardViewattr.getmTikeCount();
mTextSize = dashboardViewattr.getmTextSize();
mTextColor = dashboardViewattr.getTextColor();
mText = dashboardViewattr.getmText();
unit = dashboardViewattr.getUnit();
backgroundColor = dashboardViewattr.getBackground();
startColor = dashboardViewattr.getStartColor();
endColor = dashboardViewattr.getEndColor();
startNum = dashboardViewattr.getStartNumber();
maxNum = dashboardViewattr.getMaxNumber();
progressColor = dashboardViewattr.getProgressColor();
//初始化畫筆
paintProgress.setAntiAlias(true);//設定抗鋸齒
paintProgress.setStrokeWidth(progressHeight);//設定畫筆寬度
paintProgress.setStyle(Paint.Style.STROKE);//設定畫筆為空心
paintProgress.setStrokeCap(Paint.Cap.ROUND);//設定畫筆筆觸為圓形
paintProgress.setColor(progressColor);//設定畫筆顏色
paintProgress.setDither(true);//設定防抖動
...
現在我們來重寫我們的 onMeasure 方法, 目的是之在非EXACTLY模式下,也就是空間寬高指定為wrap_centent的時候,給他規定一個最大值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int realWidth = startMeasure(widthMeasureSpec);
int realHeight = startMeasure(heightMeasureSpec);
setMeasuredDimension(realWidth, realHeight);
}
private int startMeasure(int msSpec) {
int result = 0;
int mode = MeasureSpec.getMode(msSpec);
int size = MeasureSpec.getSize(msSpec);
if (mode == MeasureSpec.EXACTLY) {
result = size;
} else {
result = PxUtils.dpToPx(200, mContext);
}
return result;
}
終於到了最重要的環節之一了。也就是繪製環節,現在我們來重寫onDraw方法,獲取Canvas。
首先,先把座標原點移動到中心,以方便繪製
canvas.translate(mWidth/2,mHight/2);
然後,繪製錶盤,也就是該View上不會動的東西。
//繪製最外層的圓和背景色(如果設定了背景色的話)
private void drawBackground(Canvas canvas) {
//最外陰影線
canvas.drawCircle(0, 0, mWidth / 2 - 2, paintOutCircle);
canvas.save();
//背景
if (backgroundColor != 0) {
paintBackground.setColor(backgroundColor);
canvas.drawCircle(0, 0,mWidth / 2 -4, paintBackground);
}
}
根據設定的刻度數,繪製刻度(預設48)
private void drawerNum(Canvas canvas) {
canvas.save(); //記錄畫布狀態
canvas.rotate(-(180 - START_ARC + 90), 0, 0);
int numY = -mHight / 2 + OFFSET + progressHeight;
float rAngle = DURING_ARC / mTikeCount;
for (int i = 0; i < mTikeCount + 1; i++) {
canvas.save(); //記錄畫布狀態
canvas.rotate(rAngle * i,0, 0);
if (i == 0 || i % 3 ==0){
canvas.drawLine(0 , numY + 5, 0, numY + 25, paintNum);//畫長刻度線
}else {
canvas.drawLine(0 , numY + 5, 0, numY + 15, paintNum);//畫短刻度線
}
canvas.restore();
}
canvas.restore();
}
save()為儲存畫布,
restore()為恢復上次儲存的畫布,
retate()為旋轉畫布。
利用這三個方法,可以很輕鬆的繪製刻度
然後繪製中心的小圓和小環。
private void drawInPoint(Canvas canvas) {
mMinCircleRadius = mWidth / 15 ;
mMinRingRadius = mMinCircleRadius *2 + mMinCircleRadius / 20;
paintCenterRingPointer.setStrokeWidth(mMinCircleRadius);
canvas.drawCircle(0, 0, mMinCircleRadius, paintCenterCirclePointer);//中心圓點
canvas.drawCircle(0, 0, mMinRingRadius, paintCenterRingPointer);//中心小圓環
}
接下來我們來繪製能動的部分
首先,弧形progressbar
private void drawProgress(Canvas canvas, float percent) {
rectF2 = new RectF( -mWidth/2 + OFFSET, - mHight /2 + OFFSET, mWidth/2 - OFFSET, mHight/2 - OFFSET);
canvas.drawArc(rectF2, START_ARC, DURING_ARC, false, paintProgressBackground);
if (percent > 1.0f) {
percent = 1.0f; //限制進度條在彈性的作用下不會超出
}
if (!(percent <= 0.0f )) {
canvas.drawArc(rectF2, START_ARC, percent * DURING_ARC, false, paintProgress);
}
}
繪製錶針
private void drawerPointer(Canvas canvas, float percent) {
mMinCircleRadius = mWidth / 15 ;
rectF1 = new RectF( - mMinCircleRadius / 2, - mMinCircleRadius / 2, mMinCircleRadius / 2 , mMinCircleRadius / 2);
canvas.save();
float angel = DURING_ARC * (percent - 0.5f) - 180 ;
canvas.rotate(angel, 0, 0);//指標與外弧邊緣持平
Path pathPointerRight = new Path();
pathPointerRight.moveTo(0, mMinCircleRadius / 2);
pathPointerRight.arcTo(rectF1,270,-90);
pathPointerRight.lineTo(0, mHight / 2 - OFFSET- progressHeight);
pathPointerRight.lineTo(0, mMinCircleRadius / 2);
pathPointerRight.close();
Path pathPointerLeft = new Path();
pathPointerLeft.moveTo( 0, mMinCircleRadius / 2);
pathPointerLeft.arcTo(rectF1,270,90);
pathPointerLeft.lineTo(0, mHight/2 - OFFSET- progressHeight);
pathPointerLeft.lineTo(0, mMinCircleRadius / 2);
pathPointerLeft.close();
Path pathCircle = new Path();
pathCircle.addCircle(0, 0, mMinCircleRadius / 4, Path.Direction.CW);
canvas.drawPath(pathPointerLeft,paintPointerLeft);
canvas.drawPath(pathPointerRight, paintPointerRight);
canvas.drawPath(pathCircle, paintPinterCircle);
canvas.restore();
}
錶針分為三個部分:1、指標左半部分,2、指標右半部分,3、指標上的圓點。
考慮到相容性的問題,在這裡繪製指標並沒有採用Path()的布林運算 ,而是使用的Path基礎的lineTo() , arcTo() ,moveTo()等方法。一樣能實現同樣的效果。
最後,繪製文字。
private void drawText(Canvas canvas, float percent) {
float length ;
paintText.setTextSize(mTextSize);
length = paintText.measureText(mText);
canvas.drawText(mText,-length /2, mMinRingRadius*2.0F, paintText);
paintText.setTextSize(mTextSize * 1.2f);
speed = StringUtil.floatFormat(startNum + (maxNum - startNum) * percent) + unit;
length = paintText.measureText(speed);
canvas.drawText(speed, -length /2 , mMinRingRadius*2.5F, paintText);
}
可以看到,這三個方法比上面的多了個float percent 引數,我們將根據這個引數來改變指標的角度,progress的進度以及數字的變化。
這個引數其實就是seekbar傳進來的progress值。其實到現在可以算是結束了。但是指標和progress的變化都特別生硬,所以我們要給他加上一個動畫效果。
public void setPercent(int percent) {
setAnimator(percent);
}
private void setAnimator(final float percent) {
//根據變化的幅度來調整動畫時長
animatorDuration = (long) Math.abs(percent - oldPercent) * 20;
valueAnimator = ValueAnimator.ofFloat(oldPercent,percent).setDuration(animatorDuration);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//把獲取到的
DashboardView.this.percent = (float) animation.getAnimatedValue();
invalidate();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
oldPercent = percent;
}
});
valueAnimator.start();
}
在這裡我們用到了ValueAnimator,ValuAnimator本質上就是通過設定一個起始值和結束值,來取到一個從起始值到結束值的一個逐漸增長的Animation值。在draw方法中使用這個值並且不斷的重繪,就能達到一種動畫效果。
我們通過ofFloat來設定起始值和結束值,並且動態的設定了動畫時長。通過addUpdateListener(AnimatorUpdateListener)來獲取不斷變化的Animation值,並且重繪。
最後,呼叫valueAnimator.start() 我們的動畫效果就這麼完成了,但是做完這步你會發現,動畫是有了,但是這指標是勻速轉動的,不太理想。
這時候,就要用到我們的Interpolator插值器了,安卓自帶的插值器能夠使Animation值的變化產生加速增長、減速增長、先加速後減速、回彈等效果。但是安卓自帶的幾個插值器用在我們這裡也不太理想,不太符合真正的儀表盤的變化效果。
所以在這裡我們決定自定義一個插值器來滿足我們的需求。
首先確定插值器的曲線圖
數學表示式為
pow(2, -10 * x) * sin((x - factor / 4) * (2 * PI) / factor) + 1
factor = 0.4 這個我們在建構函式的時候指定
新建類SpringInterpolator,繼承自BaseInterpolator,將上面的數學表示式轉化成程式碼,完整的程式碼為:
public class SpringInterpolator implements Interpolator {
private final float mTension;
public SpringInterpolator() {
mTension = 0.4f;
}
public SpringInterpolator(float tension) {
mTension = tension;
}
@Override
public float getInterpolation(float input) {
float result = (float) (Math.pow(2,-10 * input) *
Math.sin((input - mTension / 4) * (2 * Math.PI)/mTension) + 1);
return result;
}
}
好了,我們把這個自定義插值器設定給我們的VuleAnimation就能達到預期的效果了。
當然,我們可以改變tension來改變指標的擺動效果,不過我認為0.4是一個很合理的值。
至此,我們的自定義View就算是完成了,感謝大家的耐心觀看,如果有什麼錯誤或者不足之處,還請多多指點。