Android自定義View-蜘蛛網(wǎng)屬性圖(五邊形圖)

首先看看效果圖:

這里寫圖片描述
這里寫圖片描述

先簡要說一下這里需要涉及到的知識點:

  1. 2D繪圖基礎(chǔ)。
  2. 高中基本的三角函數(shù) Sin,Cos。

參考的文章:

  1. Android自定義控件 芝麻信用分雷達圖

這里為了尊重上面這篇文章的作者,需要說明一下,下面的代碼有部分是參考上面這篇文章的。這里我學習之后有了自己的理解。做了一點小改動,然后以自己的思路來捋一捋。希望我的文字對你更有幫助,哈哈。 (Pentagon --五邊形)

繪制思路:

  1. 計算三個五邊形的五個頂點的坐標,用 path 連接起來并繪制
  2. 計算要顯示的數(shù)據(jù)的五個頂點的坐標,用 path 連接起來并繪制
  3. 繪制五條射線
  4. 計算圖標和標題的坐標位置,并繪制
  5. 繪制中間的分數(shù)

第一步:繪制三個五邊形和紅色五邊形

這里寫圖片描述

初始化成員變量

private int dataCount = 5;//多邊形維度
private float radian = (float) (Math.PI * 2 / dataCount);//每個維度的角度
private float radius;//一條星射線的長度,即是發(fā)散的五條線白線
private int centerX;//中心坐標 Y
private int centerY;//中心坐標 X
private String[] titles = {"履約能力", "信用歷史", "人脈關(guān)系", "行為偏好", "身份特質(zhì)"};//標題
private int[] icos = {R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher};//五個維度的圖標
private float[] data = {170, 180, 100, 170, 150};//五個維度的數(shù)據(jù)值
private float maxValue = 190;//每個維度的最大值
private Paint mPaintText;//繪制文字的畫筆
private int radarMargin = 40;//
private int mAlpha;//白色五邊形的透明度
private Path mPentagonPath;//記錄白色五邊形的路徑
private Paint mPentagonPaint;//繪制白色五邊形的畫筆
private Path mDataPath;//記錄紅色五邊形的路徑
private Paint mDataPaint;//繪制紅色五邊形的畫筆

構(gòu)造方法中初始化的數(shù)據(jù)

private void init() {
    mPentagonPaint = new Paint();//初始化白色五邊形的畫筆
    mPentagonPaint.setAntiAlias(true);//
    mPentagonPaint.setStrokeWidth(5);//
    mPentagonPaint.setColor(Color.WHITE);//
    mPentagonPaint.setStyle(Paint.Style.FILL_AND_STROKE);//

    mDataPaint = new Paint();//初始化紅色五邊形的畫筆
    mDataPaint.setAntiAlias(true);//
    mDataPaint.setStrokeWidth(10);//
    mDataPaint.setColor(Color.RED);//
    mDataPaint.setAlpha(150);//
    mDataPaint.setStyle(Paint.Style.STROKE);//

    mPaintText = new Paint();//初始化文字畫筆
    mPaintText.setAntiAlias(true);//
    mPaintText.setTextSize(50);//
    mPaintText.setColor(Color.WHITE);//
    mPaintText.setStyle(Paint.Style.FILL);//

    mPentagonPath = new Path();//初始化白色五邊形路徑
    mDataPath = new Path();//初始化紅色五邊形路徑
    radius = 80;//星射線的初始值,也是最小的五邊形的一條星射線的長度(后期會遞增)
    mAlpha = 150;//白色五邊形的透明度(后期后遞減)
}

初始化數(shù)據(jù)之后是通過 radius 的長度來計算五邊形五個頂點的坐標值,后期通過改變 radius 的值,來達到計算三個白色五邊形的五個頂點的值。這里給出一張圖,幫助理解。通過圖中的兩個紅色三角形就可以求出頂點的坐標了。

這里寫圖片描述

**右上角的頂點為第一個點,順時針計算,position 依次是 0,1,2,3,4 **

public Point getPoint(int position) {
    return getPoint(position, 0, 1);
}

// 參數(shù):position:頂點的位置,radarMargin:邊距,percent:星射線長度的百分比,用于計算紅色五邊形的頂點
public Point getPoint(int position, int radarMargin, float percent) {//以五邊形的中心點為坐標原點
    int x = 0;
    int y = 0;
    switch (position) {
        case 0://第一象限,右上角頂點的坐標計算
            x = (int) (centerX + (radius + radarMargin) * Math.sin(radian) * percent);
            y = (int) (centerY - (radius + radarMargin) * Math.cos(radian) * percent);
            break;
        case 1://第四象限,右下角頂點的坐標計算
            x = (int) (centerX + (radius + radarMargin) * Math.sin(radian / 2) * percent);
            y = (int) (centerY + (radius + radarMargin) * Math.cos(radian / 2) * percent);
            break;
        case 2://第三象限,左下角頂點的坐標計算
            x = (int) (centerX - (radius + radarMargin) * Math.sin(radian / 2) * percent);
            y = (int) (centerY + (radius + radarMargin) * Math.cos(radian / 2) * percent);
            break;
        case 3://第二象限,左上角頂點的坐標計算
            x = (int) (centerX - (radius + radarMargin) * Math.sin(radian) * percent);
            y = (int) (centerY - (radius + radarMargin) * Math.cos(radian) * percent);
            break;
        case 4:// Y 軸正方向頂點的計算
            x = centerX;
            y = (int) (centerY - (radius + radarMargin) * percent);
            break;
    }
    return new Point(x, y);
}

基礎(chǔ)工作都做足了,那么就進行五邊形的繪制了

private void drawPentagon(Canvas canvas) {
    for (int j = 0; j < 3; j++) {//繪制三層白色五邊形
        radius += 70;//每一層五邊形的星射線增加 70 的長度
        mAlpha -= 30;//每一層五邊形的透明度減少 30
        mPentagonPaint.setAlpha(mAlpha);
        for (int i = 0; i < dataCount; i++) {//繪制一層
            if (i == 0) {
                mPentagonPath.moveTo(getPoint(i).x, getPoint(i).y);
            } else {
                mPentagonPath.lineTo(getPoint(i).x, getPoint(i).y);
            }
        }
        mPentagonPath.close();
        canvas.drawPath(mPentagonPath, mPentagonPaint);
    }

    for (int i = 0; i < dataCount; i++) {//繪制紅色五邊形
        float percent = data[i] / maxValue;//數(shù)據(jù)值與最大值的百分比
        if (i == 0) {
            mDataPath.moveTo(getPoint(i, 0, percent).x, getPoint(i, 0, percent).y);//通過百分比計算出紅色頂點的位置
        } else {
            mDataPath.lineTo(getPoint(i, 0, percent).x, getPoint(i, 0, percent).y);
        }
    }
    mDataPath.close();
    canvas.drawPath(mDataPath, mDataPaint);
}

第二步:繪制五條星射線

先來看看這一步的效果圖:

這里寫圖片描述

繪制好五邊形之后 radius 的值已經(jīng)為最大五邊形的星射線的長度了。

private void drawFiveLine(Canvas canvas) {
    mPentagonPaint.setColor(Color.WHITE);//設(shè)置顏色為白色
    mPentagonPaint.setStrokeWidth(2);//設(shè)置寬度為2
    for (int i = 0; i < dataCount; i++) {
        canvas.drawLine(centerX, centerY, getPoint(i).x, getPoint(i).y, mPentagonPaint);//繪制
    }
}

第三步:繪制五個標題

先來看看這一步的效果圖:

這里寫圖片描述

在這一步,相對難一點的就是坐標的計算了。第一個頂點的坐標經(jīng)過添加 radarMargin 值之后就可以直接使用了,其他頂點還需要經(jīng)過計算得到。這里其實就是要計算每一個 Title 文字左下角的坐標。那么計算坐標的代碼是這樣的:

private void drawTitle(Canvas canvas) {
    for (int i = 0; i < dataCount; i++) {
        int x = getPoint(i, radarMargin, 1).x;//獲取添加 radarMargin 值之后的 X 坐標的指
        int y = getPoint(i, radarMargin, 1).y;//獲取添加 radarMargin 值之后的 Y 坐標的指
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), icos[i]);
        int iconHeight = bitmap.getHeight();
        int titleWidth = (int) mPaintText.measureText(titles[i]);
        switch (i) {
            case 1://說明一下為什么是 iconHeight / 2 ,主要是因為這樣會比較好看
                y += iconHeight / 2;
                break;
            case 2:
                x -= titleWidth;
                y += iconHeight / 2;
                break;
            case 3:
                x -= titleWidth;
                break;
            case 4:
                x -= titleWidth / 2;
                break;
        }
        canvas.drawText(titles[i], x, y, mPaintText);
    }
}

第四步:繪制圖標

先來看看這一步的效果:

這里寫圖片描述

這一步也是要進行坐標的計算,主要的計算出放置圖標左上角的坐標值。這里還是相對簡單一點,不需要用到三角函數(shù)。

private void drawIcon(Canvas canvas) {
    for (int i = 0; i < dataCount; i++) {
        int x = getPoint(i, radarMargin, 1).x;//獲取添加 radarMargin 值之后的 X 坐標的指
        int y = getPoint(i, radarMargin, 1).y;//獲取添加 radarMargin 值之后的 Y 坐標的指
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), icos[i]);
        int iconHeight = bitmap.getHeight();
        int iconWidth = bitmap.getWidth();
        int titleWidth = (int) mPaintText.measureText(titles[i]);
        switch (i) {
            case 0:
                x += (titleWidth - iconWidth) / 2;
                y -= (iconHeight + getTextHeight(titles[i]));
                break;
            case 1:
                x += (titleWidth - iconWidth) / 2;
                y -= (iconHeight / 2 + getTextHeight(titles[i]));
                break;
            case 2:
                x -= titleWidth - (titleWidth - iconWidth) / 2;
                y -= (iconHeight / 2 + getTextHeight(titles[i]));
                break;
            case 3:
                x -= titleWidth - (titleWidth - iconWidth) / 2;
                y -= (iconHeight + getTextHeight(titles[i]));
                break;
            case 4:
                x -= (iconHeight / 2);
                y -= (iconHeight + getTextHeight(titles[i]));
                break;
        }
        canvas.drawBitmap(bitmap, x, y, mPaintText);
    }
}

第五步:繪制中心點的分數(shù)

這一步完成之后就可以得到最終效果了,就是圖一的效果:

這里寫圖片描述
這里寫圖片描述

文字的坐標是中心點,那么計算出文字的寬度和高度就可以居中顯示文字了。

private void drawScore(Canvas canvas) {
    mPaintText.setColor(getResources().getColor(R.color.colorAccent));
    int score = 0;
    for (int i = 0; i < data.length; i++) {//累加分數(shù)值
        score += data[i];
    }
    String str_score = String.valueOf(score);
    Paint.FontMetrics fm = mPaintText.getFontMetrics();//用于計算文字的高度
    canvas.drawText(str_score, centerX - mPaintText.measureText(str_score) / 2, (centerY + (int) Math.ceil(fm.descent - fm.ascent) / 2), mPaintText);
}

總結(jié)

一步步下來,對自定義 view 也有了進一步的了解。感覺需要更多的練習才能完全 hold 住。如果文中有什么知識點是錯誤的或者更好的實現(xiàn)方法,請及時聯(lián)系我進行修改,以免誤導別人。謝謝。那么完整的代碼是這樣的。

/**
 * Created by zone on 2017/4/9.
 */

public class PentagonView extends View {
    private int dataCount = 5;//多邊形維度,這里是五邊形
    private float radian = (float) (Math.PI * 2 / dataCount);//每個維度的角度
    private float radius;//一條星射線的長度,即是發(fā)散的五條線白線
    private int centerX;//中心坐標 Y
    private int centerY;//中心坐標 X
    private String[] titles = {"履約能力", "信用歷史", "人脈關(guān)系", "行為偏好", "身份特質(zhì)"};//標題
    private int[] icos = {R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher};//五個維度的圖標
    private float[] data = {170, 180, 100, 170, 150};//五個維度的數(shù)據(jù)值
    private float maxValue = 190;//每個維度的最大值
    private Paint mPaintText;//繪制文字的畫筆
    private int radarMargin = 40;//
    private int mAlpha;//白色五邊形的透明度
    private Path mPentagonPath;//記錄白色五邊形的路徑
    private Paint mPentagonPaint;//繪制白色五邊形的畫筆
    private Path mDataPath;//記錄紅色五邊形的路徑
    private Paint mDataPaint;//繪制紅色五邊形的畫筆


    public PentagonView(Context context) {
        super(context);
        init();
    }

    public PentagonView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PentagonView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public PentagonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    private void init() {
        mPentagonPaint = new Paint();//初始化白色五邊形的畫筆
        mPentagonPaint.setAntiAlias(true);//
        mPentagonPaint.setStrokeWidth(5);//
        mPentagonPaint.setColor(Color.WHITE);//
        mPentagonPaint.setStyle(Paint.Style.FILL_AND_STROKE);//

        mDataPaint = new Paint();//初始化紅色五邊形的畫筆
        mDataPaint.setAntiAlias(true);//
        mDataPaint.setStrokeWidth(10);//
        mDataPaint.setColor(Color.RED);//
        mDataPaint.setAlpha(150);//
        mDataPaint.setStyle(Paint.Style.STROKE);//

        mPaintText = new Paint();//初始化文字畫筆
        mPaintText.setAntiAlias(true);//
        mPaintText.setTextSize(50);//
        mPaintText.setColor(Color.WHITE);//
        mPaintText.setStyle(Paint.Style.FILL);//

        mPentagonPath = new Path();//初始化白色五邊形路徑
        mDataPath = new Path();//初始化紅色五邊形路徑
        radius = 80;//星射線的初始值,也是最小的五邊形的一條星射線的長度(后期會遞增)
        mAlpha = 150;//白色五邊形的透明度(后期后遞減)
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(getResources().getColor(R.color.colorAccent));
        centerX = getWidth() / 2;
        centerY = getHeight() / 2;
        drawPentagon(canvas);//繪制白色五邊形和紅色五邊形
        drawFiveLine(canvas);//繪制五條星射線
        drawTitle(canvas);//繪制五個標題
        drawIcon(canvas);//繪制五個圖標
        drawScore(canvas);//繪制中間的分數(shù)
    }

    private void drawPentagon(Canvas canvas) {
        for (int j = 0; j < 3; j++) {//繪制三層白色五邊形
            radius += 70;//每一層五邊形的星射線增加 70 的長度
            mAlpha -= 30;//每一層五邊形的透明度減少 30
            mPentagonPaint.setAlpha(mAlpha);
            for (int i = 0; i < dataCount; i++) {//繪制一層
                if (i == 0) {
                    mPentagonPath.moveTo(getPoint(i).x, getPoint(i).y);
                } else {
                    mPentagonPath.lineTo(getPoint(i).x, getPoint(i).y);
                }
            }
            mPentagonPath.close();
            canvas.drawPath(mPentagonPath, mPentagonPaint);
        }

        for (int i = 0; i < dataCount; i++) {//繪制紅色五邊形
            float percent = data[i] / maxValue;//數(shù)據(jù)值與最大值的百分比
            if (i == 0) {
                mDataPath.moveTo(getPoint(i, 0, percent).x, getPoint(i, 0, percent).y);//通過百分比計算出紅色頂點的位置
            } else {
                mDataPath.lineTo(getPoint(i, 0, percent).x, getPoint(i, 0, percent).y);
            }
        }
        mDataPath.close();
        canvas.drawPath(mDataPath, mDataPaint);
    }

    private void drawFiveLine(Canvas canvas) {
        mPentagonPaint.setColor(Color.WHITE);//設(shè)置顏色為白色
        mPentagonPaint.setStrokeWidth(2);//設(shè)置寬度為2
        for (int i = 0; i < dataCount; i++) {
            canvas.drawLine(centerX, centerY, getPoint(i).x, getPoint(i).y, mPentagonPaint);//繪制
        }
    }

    private void drawIcon(Canvas canvas) {
        for (int i = 0; i < dataCount; i++) {
            int x = getPoint(i, radarMargin, 1).x;//獲取添加 radarMargin 值之后的 X 坐標的指
            int y = getPoint(i, radarMargin, 1).y;//獲取添加 radarMargin 值之后的 Y 坐標的指
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), icos[i]);
            int iconHeight = bitmap.getHeight();
            int iconWidth = bitmap.getWidth();
            int titleWidth = (int) mPaintText.measureText(titles[i]);
            switch (i) {
                case 0:
                    x += (titleWidth - iconWidth) / 2;
                    y -= (iconHeight + getTextHeight(titles[i]));
                    break;
                case 1:
                    x += (titleWidth - iconWidth) / 2;
                    y -= (iconHeight / 2 + getTextHeight(titles[i]));
                    break;
                case 2:
                    x -= titleWidth - (titleWidth - iconWidth) / 2;
                    y -= (iconHeight / 2 + getTextHeight(titles[i]));
                    break;
                case 3:
                    x -= titleWidth - (titleWidth - iconWidth) / 2;
                    y -= (iconHeight + getTextHeight(titles[i]));
                    break;
                case 4:
                    x -= (iconHeight / 2);
                    y -= (iconHeight + getTextHeight(titles[i]));
                    break;
            }
            canvas.drawBitmap(bitmap, x, y, mPaintText);
        }
    }

    private int getTextHeight(String text) {
        Paint.FontMetrics fm = mPaintText.getFontMetrics();
        return (int) Math.ceil(fm.descent - fm.ascent);
    }

    private void drawTitle(Canvas canvas) {
        for (int i = 0; i < dataCount; i++) {
            int x = getPoint(i, radarMargin, 1).x;//獲取添加 radarMargin 值之后的 X 坐標的指
            int y = getPoint(i, radarMargin, 1).y;//獲取添加 radarMargin 值之后的 Y 坐標的指
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), icos[i]);
            int iconHeight = bitmap.getHeight();
            int titleWidth = (int) mPaintText.measureText(titles[i]);
            switch (i) {
                case 1://說明一下為什么是 iconHeight / 2 ,主要是因為這樣會比較好看
                    y += iconHeight / 2;
                    break;
                case 2:
                    x -= titleWidth;
                    y += iconHeight / 2;
                    break;
                case 3:
                    x -= titleWidth;
                    break;
                case 4:
                    x -= titleWidth / 2;
                    break;
            }
            canvas.drawText(titles[i], x, y, mPaintText);
        }
    }

    private void drawScore(Canvas canvas) {
        mPaintText.setColor(getResources().getColor(R.color.colorAccent));
        int score = 0;
        for (int i = 0; i < data.length; i++) {//累加分數(shù)值
            score += data[i];
        }
        String str_score = String.valueOf(score);
        Paint.FontMetrics fm = mPaintText.getFontMetrics();//用于計算文字的高度
        canvas.drawText(str_score, centerX - mPaintText.measureText(str_score) / 2, (centerY + (int) Math.ceil(fm.descent - fm.ascent) / 2), mPaintText);
    }

    public Point getPoint(int position) {
        return getPoint(position, 0, 1);
    }

// 右上角的頂點為第一個點,順時針計算,position 依次是 0,1,2,3,4
// 參數(shù):position:頂點的位置,radarMargin:邊距,percent:星射線長度的百分比,用于計算紅色五邊形的頂點
    public Point getPoint(int position, int radarMargin, float percent) {//以五邊形的中心點為坐標原點
        int x = 0;
        int y = 0;
        switch (position) {
            case 0://第一象限,右上角頂點的坐標計算
                x = (int) (centerX + (radius + radarMargin) * Math.sin(radian) * percent);
                y = (int) (centerY - (radius + radarMargin) * Math.cos(radian) * percent);
                break;
            case 1://第四象限,右下角頂點的坐標計算
                x = (int) (centerX + (radius + radarMargin) * Math.sin(radian / 2) * percent);
                y = (int) (centerY + (radius + radarMargin) * Math.cos(radian / 2) * percent);
                break;
            case 2://第三象限,左下角頂點的坐標計算
                x = (int) (centerX - (radius + radarMargin) * Math.sin(radian / 2) * percent);
                y = (int) (centerY + (radius + radarMargin) * Math.cos(radian / 2) * percent);
                break;
            case 3://第二象限,左上角頂點的坐標計算
                x = (int) (centerX - (radius + radarMargin) * Math.sin(radian) * percent);
                y = (int) (centerY - (radius + radarMargin) * Math.cos(radian) * percent);
                break;
            case 4:// Y 軸正方向頂點的計算
                x = centerX;
                y = (int) (centerY - (radius + radarMargin) * percent);
                break;
        }
        return new Point(x, y);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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