如果你對(duì)自定義View還不是很了解,那么這篇文章將從實(shí)戰(zhàn)的角度帶你一步一步的編寫(xiě)出一個(gè)符合規(guī)范的自定義View。
需求:假設(shè)有一個(gè)病人,他不定時(shí)的將自己的血壓值錄入到我們的客戶(hù)端,而我們要做的就是將他近七天所錄入的值展示成一張折線(xiàn)趨勢(shì)圖,區(qū)分異常和正常值。
以上就是我們的需求,那我們就通過(guò)自定義View來(lái)實(shí)現(xiàn)它:

我們知道自定義View的時(shí)候最重要的方法就是draw()方法,我們?cè)陧?yè)面上所展示的效果就是通過(guò)這個(gè)方法來(lái)實(shí)現(xiàn)的。當(dāng)然還有onMeasure()方法,這個(gè)是對(duì)控件進(jìn)行測(cè)量的,可能你會(huì)遇到這種情況,當(dāng)你在xml文件中將其寬度設(shè)置為warp_content的時(shí)候,你的控件在頁(yè)面上啥也沒(méi)有,這是為毛???原因就是可能沒(méi)有在onMeasure()方法里做相應(yīng)的處理。那么我們先來(lái)看看如何處理這個(gè)問(wèn)題:
//測(cè)量View的寬高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//如果寬高都是warp_content時(shí),設(shè)置控件的寬高的大小
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(400, 600);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(400, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 600);
}
}
在上面的代碼中,先是獲取到控件寬和高的MeasureSpec的方式,然后進(jìn)行判斷是否為MeasureSpec.AT_MOST模式,如果是則調(diào)用父類(lèi)中setMeasuredDimension()函數(shù)(不了解的話(huà)可以看看View的相關(guān)源碼,測(cè)量寬高最后調(diào)用的就是這個(gè)方法),可以看出我設(shè)置默認(rèn)的寬高分別為400和600。這段代碼可能都已經(jīng)成為模板了,可以直接復(fù)制粘貼的。
好了最重要的就是如何來(lái)繪制這個(gè)這折線(xiàn)圖?我們一步一步來(lái),首先我們得繪制一下對(duì)這個(gè)折線(xiàn)圖的說(shuō)明:
/**
* 繪制圖的說(shuō)明
* @param canvas
*/
private void drawDes(Canvas canvas) {
canvas.drawText(normalText, getPaddingLeft() + getTextWidth(mList.get(0), mTextPaint) * 2, (float) getTextHeight(normalText, mTextPaint) + getPaddingTop(), mTextPaint);
mCirclePaint.setColor(Color.parseColor(normalColor));
canvas.drawCircle(getPaddingLeft() + getTextWidth(mList.get(0), mTextPaint) * 3 + (float) getTextWidth(normalText, mTextPaint) / 2, (float) getTextHeight(normalText, mTextPaint) - r + getPaddingTop(), r, mCirclePaint);
canvas.drawText(unusualText, getPaddingLeft() + getTextWidth(mList.get(0), mTextPaint) * 3 + getTextWidth(normalText, mTextPaint), (float) getTextHeight(normalText, mTextPaint) + getPaddingTop(), mTextPaint);
mCirclePaint.setColor(Color.parseColor(unusualColor));
canvas.drawCircle(getPaddingLeft() + getTextWidth(mList.get(0), mTextPaint) * 3 + (float) getTextWidth(normalText, mTextPaint) * 5 / 2, (float) getTextHeight(normalText, mTextPaint) - r + getPaddingTop(), r, mCirclePaint);
}
這個(gè)就是將圖中的正常和異常這幾個(gè)文字和圓點(diǎn)畫(huà)上去,調(diào)用的是繪制文字drawText和繪制圓點(diǎn)drawCircle這兩個(gè)方法,為了能夠支持padding,我們必須將其考慮進(jìn)去,這里主要就是計(jì)算出文字本身寬高,我們可以通過(guò)計(jì)算出包圍文字的最小矩形,從而得到文字的寬高:
/**
* @param text 繪制的文字
* @param paint 畫(huà)筆
* @return 文字的寬度
*/
public int getTextWidth(String text, Paint paint) {
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
int width = bounds.left + bounds.width();
return width;
}
說(shuō)明繪制完之后,接著來(lái)繪制圖的坐標(biāo)刻度以及橫線(xiàn):
/**
* 繪制坐標(biāo)軸
* @param canvas
*/
private void drawAxis(Canvas canvas) {
//計(jì)算出y軸刻度的間距
float h = (float) ((height - getTextHeight(normalText, mTextPaint) - getTextHeight(mList.get(0), mTextPaint) * 2.0) / mList.size());
for (int i = 0; i < mList.size(); i++) {
canvas.drawText(mList.get(i), (float) getPaddingLeft() + getTextWidth(mList.get(0), mTextPaint) / 2, getTextHeight(normalText, mTextPaint) * 2 + getPaddingTop() + h * i, mTextPaint);
canvas.drawLine(getPaddingLeft() + getTextWidth(mList.get(0), mTextPaint) * 2,
getTextHeight(normalText, mTextPaint) * 2 + getPaddingTop() + h * i - (float) getTextHeight(mList.get(0), mTextPaint) / 4,
getWidth() - getPaddingRight(),
getTextHeight(normalText, mTextPaint) * 2 + getPaddingTop() + h * i - (float) getTextHeight(mList.get(0), mTextPaint) / 4,
mTextPaint);
//Log.e(TAG, "drawXY: xi="+(getPaddingLeft()+getTextWidth(mList.get(0), mTextPaint) * 2));
// Log.e(TAG, "drawXY: yj="+(getTextHeight(normalText, mTextPaint)*2 + getPaddingTop() + h * i - (float) getTextHeight(mList.get(0), mTextPaint) / 4));
if (i == mList.size() - 1) {
float v = (width - (float) getTextHeight(mList.get(0), mTextPaint) * 2) / 7;
// Log.e(TAG, "drawData: w2="+v);
for (int j = 0; j < mDateList.size(); j++) {
canvas.drawLine(getPaddingLeft() + getTextWidth(mList.get(0), mTextPaint) * 2 + v * j,
getTextHeight(normalText, mTextPaint) * 2 + getPaddingTop() + h * i - (float) getTextHeight(mList.get(0), mTextPaint) / 4,
getPaddingLeft() + getTextWidth(mList.get(0), mTextPaint) * 2 + v * j,
getTextHeight(normalText, mTextPaint) * 2 + getPaddingTop() + h * i - (float) getTextHeight(mList.get(0), mTextPaint) / 4 - 5,
mTextPaint);
canvas.drawText(mDateList.get(j).substring(8, 10), getPaddingLeft() + getTextWidth(mList.get(0), mTextPaint) * 2 + v * j + v / 2 - (float) getTextWidth(mDateList.get(0).substring(8, 10), mTextPaint) / 2, (float) getTextHeight(mDateList.get(0), mTextPaint) * 2 + getTextHeight(normalText, mTextPaint) * 2 + getPaddingTop() + h * i, mTextPaint);
}
}
}
}
在這里我先畫(huà)出y軸上的刻度和橫線(xiàn),然后在將x軸的刻度和數(shù)值畫(huà)上,這里需要我們計(jì)算出每根橫線(xiàn)之間的間距就是(控件的高度-paddingTop-文字說(shuō)明的高度-刻度高度/2-paddingBottom-x軸刻度的高度)/橫線(xiàn)數(shù)量。
坐標(biāo)軸畫(huà)完之后,接下來(lái)就是畫(huà)折線(xiàn)了,畫(huà)折線(xiàn)之前需要把要展示的數(shù)據(jù)轉(zhuǎn)換成相應(yīng)的點(diǎn)坐標(biāo)。首先求出x坐標(biāo),因?yàn)閤軸表示的是時(shí)間,那么我們可以利用錄入時(shí)時(shí)間和前七天的時(shí)間差來(lái)?yè)Q算出比例從而得出點(diǎn)的x坐標(biāo),再來(lái)看y坐標(biāo),同理我們根據(jù)y軸上的最大和最小刻度差和控件的實(shí)際高度來(lái)?yè)Q算出y坐標(biāo)。
求出所有數(shù)據(jù)相對(duì)應(yīng)的點(diǎn)坐標(biāo),接下來(lái)就好辦了,直接進(jìn)行繪制。但需要主要一點(diǎn)就是,我們應(yīng)該先畫(huà)折線(xiàn),然后再畫(huà)圓點(diǎn),讓圓點(diǎn)覆蓋在折線(xiàn)上面。如果一起繪制的話(huà),那可能要麻煩些,需要用到勾股定理來(lái)得出線(xiàn)段的起點(diǎn)和終點(diǎn)。這里除了可以使用canvas.drawLine方法外,還可以使用canvas.drawPaint();來(lái)將點(diǎn)連接起來(lái)。
for (int i = 0; i < mPointentities.size() - 1; i++) {
PointEntity pointEntity = mPointentities.get(i);
PointEntity pointEntityNext = mPointentities.get(i + 1);
canvas.drawLine(pointEntity.getX(), pointEntity.getY(), pointEntityNext.getX(), pointEntityNext.getY(), mLinePaint);
}
for (int i = 0; i < mPointentities.size(); i++) {
PointEntity pointEntity = mPointentities.get(i);
Log.e(TAG, "drawData: x="+pointEntity.getX()+" y="+pointEntity.getY() +"maxRisk="+maxRisk+" minRisk="+minRisk);
if (pointEntity.getY() <=maxRisk &&pointEntity.getY() >=minRisk)
mCirclePaint.setColor(Color.parseColor(normalColor));
else
mCirclePaint.setColor(Color.parseColor(unusualColor));
canvas.drawCircle(pointEntity.getX(), pointEntity.getY(), r, mCirclePaint);
}
以上就差不多就是繪制這個(gè)View的主要工作流程,剛開(kāi)始寫(xiě)博客,不知道如何入手,寫(xiě)的有點(diǎn)混亂(勿噴?。。。?,完整代碼已經(jīng)上傳到gitHub上了。