Android實(shí)戰(zhàn)之自定義View折線(xiàn)圖

如果你對(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上了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容