1. 程式人生 > >自己定義View之Chart圖標系列(1)——點陣圖

自己定義View之Chart圖標系列(1)——點陣圖

tint 發現 坐標軸 畫的 tracking androi mit def dot

近期要做一些圖表類的需求,一開始就去github上看了看,發現開源的圖表框架還是蠻多的。可是非常少有全然符合我的需求的。另外就是使用起來比較麻煩。所以就決定自己來造輪子了~~~
今天要介紹的就是Android圖標系列中點陣圖(姑且這麽叫著吧╮(╯▽╰)╭)的畫法。
效果圖例如以下:
技術分享

需求:
1. 給出幾個點 畫出坐標軸(用虛線)
2. 畫出相應的點 在點的上方標出數值
3. 下方要顯示個數值表示的意義
4. 重點!!

!動態計算坐標軸,多余的坐標不能顯示。

前三條好理解。第四條啥意思呢~
(比方說,我們數據是{10.1,12.5, 20.2, 15.1} 那麽我們的坐標軸起點就應該是從10開始畫,而不是0!)例如以下圖所看到的
技術分享

接下來我們就來一步步實現這些需求吧~
1.定義數據模型

**
 * Created by JK on 2016/1/18.
 * 點陣折線圖的數據模型
 */
public class DotChartItem {
    public int color;//顏色
    public float value;//值
    public String title;//標題

    public DotChartItem(int color, float value, String title) {
        this.color = color;
        this.value = value
; this.title = title; } }

2.自己定義View —— JKLineChart

首先是初始化部分

/**
 * Created by JK on 16/1/18.
 * 用毛的開源項目自己寫控件之點陣圖
 * 這是一個點陣圖
 */
public class JKLineChart extends View {

    private static final String TAG = "JKLineChart";
    private Context mContext;
    private Paint mCoordPaint;//坐標線的Paint
private Paint mIndicPaint; //點圖的Paint private Paint mHintTitlePaint; //提示塊的Paint private int mWidth; private int mHeight; private int mCoordColor = Color.GRAY; //坐標線的color private int mPadding = 20; private int mGapHeight = 50;//坐標軸之間的間隔 private float mTextX = 0f; //提示模塊中。第一個方塊的X坐標(和坐標上的字分離) private float[] mMinAndMaxPointY = new float[2]; //最低點和最高點的Y坐標(參考點 用來計算方塊的坐標) private List<LineChartItem> mItemList; //數據集合 private List<LineChartItem> mTypeSet;//下方提示模塊的數據集合 private float[] values; enum IndicatorType { //點陣圖 圖形類型 CIRCLE, //圓 RECTANGLE //方塊 } private IndicatorType mIndicatorType = IndicatorType.RECTANGLE; private int mGap = 5; //坐標軸之間的間隔 private int mIndicWidth = 5; //小方塊的寬度 或者小圓圈的直徑 //以下2個值用來做動畫 private int mInitWidth = 0; private int mSpeed = 1; private boolean isShowHintBlock = true; //是否顯示底下的提示模塊 private int mHintTitleHeight = 30; //提示模塊高度 private final double EP = 0.000000001; public JKLineChart(Context context) { this(context, null); } public JKLineChart(Context context, AttributeSet attrs) { this(context, attrs, 0); } public JKLineChart(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.JKLineChart, 0, 0); try { isShowHintBlock = typedArray.getBoolean(R.styleable.JKLineChart_showHintBlock, true); //坐標軸之間的間隔 默覺得5 mGap = typedArray.getInt(R.styleable.JKLineChart_coordGap, mGap); //點是用圓圈還是用方塊來畫 int isRound = typedArray.getInt(R.styleable.JKLineChart_indicatorType, 1); Log.d(TAG, "ROUND :" + isRound); if (isRound == 0) { mIndicatorType = IndicatorType.CIRCLE; } else { mIndicatorType = IndicatorType.RECTANGLE; } } finally { typedArray.recycle(); } init(context); }

上面的操作主要是定義一些變量。並從構造函數中獲取我們的自己定義屬性

 private void init(Context context) {
        this.mContext = context;
        mItemList = new ArrayList<LineChartItem>();
        //mPadding = dip2px(context,mPadding);
        mIndicWidth = dip2px(context, mIndicWidth);
        mHintTitleHeight = dip2px(context, 40);
        mGapHeight = dip2px(context,mGapHeight);
        initPaint();
    }
    //初始化畫筆
    private void initPaint() {
        mIndicPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCoordPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mHintTitlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCoordPaint.setColor(mCoordColor);
        mCoordPaint.setTextSize(40);
    }
    @Override
    protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
        this.mWidth = width;
        this.mHeight = height;
        if (isShowHintBlock) {
            mHeight = mHeight - mHintTitleHeight;
        }
        Log.d(TAG, "onSizeChanged");

    }

mHeight 代表的是點陣圖的高度(不包含下方的提示部分)。假設要顯示下方的提示部分,就要減去提示部分的高度。
以下就是最重點。最基本的onDraw方法嘍

private void animatedDraw(Canvas canvas) {
        if (mItemList == null || mItemList.size() == 0)
            return;

        drawCoordText(canvas);
        drawPoint(canvas);
        if (isShowHintBlock) {
            drawHintTitle(canvas);
        }

    }

分三步走,
第一 drawCoordText畫坐標線
第二 drawPoint畫點
第三 drawHintTitle畫下方提示部分

/**
     * 畫坐標線
     * @param canvas
     */
    private void drawCoordText(Canvas canvas) {
        //獲取坐標上下限的值
        float[] array = getMinAndMaxCoord();
        //坐標軸總數量
        float totalCount = (array[1] -array[0])/ mGap +1;
        for (float i = array[1], count = 0; i >= array[0]; i = i - mGap, count++) {
            String text = (int) i + "";
            Rect textRect = new Rect();
            mCoordPaint.getTextBounds(text, 0, text.length(), textRect);
            float y = mPadding*3 + mGapHeight*count;
            if (count == 0) {
                mMinAndMaxPointY[1] = y; //最高點的坐標
            } else if (count == totalCount - 1) {
                mMinAndMaxPointY[0] = y; //最低點的坐標
            }
            canvas.drawText(text, mPadding, y, mCoordPaint);
            //canvas.drawLine(mPadding,y,mWidth,y,mCoordPaint);
            //畫虛線坐標線
            PathEffect effects = new DashPathEffect(new float[]{5, 10}, 1);
            Path path = new Path();
            path.moveTo(mPadding * 2f + textRect.width(), y - textRect.height() / 2);
            if (mTextX >= -EP && mTextX <= EP) {
                mTextX = mPadding * 2f + textRect.width();
            }
            path.lineTo(mWidth - mPadding, y - textRect.height() / 2);
            mCoordPaint.setStyle(Paint.Style.STROKE);
            mCoordPaint.setPathEffect(effects);
            canvas.drawPath(path, mCoordPaint);

        }
    }

這裏有兩個輔助函數非常重要!

。!堪稱畫坐標線的核心!

前面提到過,我們的坐標軸是依據傳入的數值動態計算的,而不是直接從0開始畫的。

 private float[] getMaxAndMin(float[] array) {
        float min, max;
        min = max = array[0];
        float[] result = new float[2];
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + " ");
            if (Float.compare(array[i], max) > 0)   // 推斷最大值
                max = array[i];
            if (Float.compare(array[i], min) < 0)   // 推斷最小值
                min = array[i];
        }
        result[0] = min;
        result[1] = max;
        return result;
    }

上面這個函數用來獲取一個數組中的最大最小值,不管數組是否排序,這個比較簡單。

/**
     * 動態計算最小坐標值和最大坐標值x
     * ps:{4.9f,6f,7f,8f,9.1f}  坐標最小值為  = 0; 坐標最大值 = 10;
     *
     * @return
     */
    private float[] getMinAndMaxCoord() {
        float[] array = getMaxAndMin(values);
        float[] result = new float[2];
        float min = array[0];
        float max = array[1];
        result[0] = (Math.round((min - 0.5f)) / mGap * mGap); //坐標值最小值
        result[1] = ((int) (floor((max + mGap - 0.5f)) / mGap) * mGap); //坐標值最大值
        return result;
    }

上面這個函數用來計算坐標軸的最小值和最大值,
比方數組是{4.9f,6f,7f,8f,9.1f} 坐標最小值為 = 0; 坐標最大值 = 10;
數組是{5.1f,6f,7f,8f,10.1f} 坐標最小值為 = 5; 坐標最大值 = 15;
floor是我們重寫了地板除法

 /**
     * 重寫floor除法
     *
     * @param value
     * @return
     */
    private float floor(float value) {
        float result = 0f;
        float tail = value % 1;
        int integer = (int) (value - tail);
        if (Float.compare(tail, 0.5f) > 0) {

            return integer + 1;
        } else if (Float.compare(tail, 0.5f) <= 0) {

            return integer;
        }
        return result;
    }

第二步: 畫點

 /**
     * 畫小方塊
     *
     * @param canvas
     */
    private void drawPoint(Canvas canvas) {
        float totalWidth = mWidth - 2 * mPadding - mTextX;  //全部方塊所能占領的總面積
        int size = values.length;
        for (int i = 0; i <size; i++) {
            float value = values[i];
            int color = mItemList.get(i).color;
            mIndicPaint.setColor(color);
            mIndicPaint.setTextSize(40);
            String text = value + "";
            Rect textRect = new Rect();
            mIndicPaint.getTextBounds(text, 0, text.length(), textRect);
            if (mIndicatorType == IndicatorType.RECTANGLE) {
                float top = getPointYByValue(value);
                float left = mPadding + mTextX + totalWidth / size * i + (totalWidth / size - mInitWidth) / 2;
                float right = left + mInitWidth;
                float bottom = top + mInitWidth;
                RectF indicRect = new RectF(left, top, right, bottom);
                canvas.drawText(text, left, top - textRect.height(), mIndicPaint);
                canvas.drawRect(indicRect, mIndicPaint);
            }
        }
        if (mInitWidth <= mIndicWidth) {
            mInitWidth += mSpeed;
            invalidate();
        }
    }

技術分享
mTextX是右側方塊區域的起始坐標。用來與坐標數字分離,totalWidth是全部點所能占領的寬度,依據點的數量將totalWidth等分,然後居中顯示在各自的區域裏。
這裏我們僅僅實現了點為方塊的樣式。點為圓圈的樣式各位看官能夠自己嘗試去實現以下。

getPointYByValue函數比較重要。是依據點的值來計算坐標的。




 /**
     * 依據值來計算方塊的y坐標
     *
     * @param value
     * @return
     */
    private float getPointYByValue(float value) {
        float[] array = getMinAndMaxCoord();
        float diffY = mMinAndMaxPointY[0] - mMinAndMaxPointY[1]; //坐標值之間的差值
        float diffValue = array[1] - array[0];
        float y = mMinAndMaxPointY[1] + diffY/diffValue* (array[1]-value)-mIndicWidth*3/2;
        return y;
    }

最後 就是畫底部的提示區域了

 public void getHintTitleList() {
        mTypeSet = new ArrayList<LineChartItem>(mItemList);
        for (int i = 0; i < mTypeSet.size() - 1; i++) {
            for (int j = mTypeSet.size() - 1; j > i; j--) {
                if (mTypeSet.get(j).title.equals(mTypeSet.get(i).title)) {
                    mTypeSet.remove(j);
                }
            }
        }
        Collections.sort(mTypeSet, new Comparator<LineChartItem>() {
            @Override
            public int compare(LineChartItem lhs, LineChartItem rhs) {
                return -(Float.compare(lhs.value, rhs.value));
            }
        });
    }

    private void drawHintTitle(Canvas canvas) {
        getHintTitleList();
        int totalWidth = mWidth - mPadding * 2;
        int width = totalWidth / mTypeSet.size();
        float startY = mMinAndMaxPointY[0] + mPadding * 3;
        for (int i = 0; i < mTypeSet.size(); i++) {
            //draw
            LineChartItem type = mTypeSet.get(i);
            String text = type.title;
            int color = type.color;
            mHintTitlePaint.setColor(color);
            mHintTitlePaint.setTextSize(40);
            Rect textRect = new Rect();
            mHintTitlePaint.getTextBounds(text, 0, text.length(), textRect);
            float x = (width - (mIndicWidth*2 + mPadding + textRect.width())) / 2 + i * width;
            RectF indicRect = new RectF(x + mPadding, startY, x + mIndicWidth*2 + mPadding, startY + mIndicWidth*2);
            canvas.drawRect(indicRect, mHintTitlePaint);
            mHintTitlePaint.setColor(mCoordColor);
            canvas.drawText(text, mPadding * 3 + x, startY + mIndicWidth*2, mHintTitlePaint);
        }

    }

getHintTitleList將我們傳入的數組篩選出提示標題,並依照數值排序。

最最後~!

!。,因為我們的坐標可能有非常多。0-——無窮都有可能,所以我們自己定義View的高度不是固定的,須要動態計算出來。

public void setItemList(List<LineChartItem> list) {
        if(list.size()==0 || list == null)
            return;
        mItemList.clear();
        mItemList.addAll(list);
        int size = mItemList.size();
        values = new float[mItemList.size()];
        for (int i = 0; i < size; i++) {
            values[i] = mItemList.get(i).value;
        }
        computerHeight();
        invalidate();
    }

    private void computerHeight() {
        float[] array = getMinAndMaxCoord();
        //坐標軸總數量
        float totalCount = (array[1] -array[0])/ mGap  ;
        //上面折線圖的最大Y坐標
        float y = mPadding*3 + mGapHeight*totalCount;
        //以下提示部分的起始Y坐標
        float startY = y + mPadding * 3;
        int height = (int) (startY+mIndicWidth*2+mPadding*2);
        ViewGroup.LayoutParams lp = getLayoutParams();
        lp.height = height;
        this.setLayoutParams(lp);
        Log.d(TAG,"HEIGHT:"+height);

    }

每次我們填充時間的時候,都會先調用computerHeight來動態計算視圖高度

使用方式 SO EASY~!

 LineChartItem item1 = new LineChartItem(Color.parseColor("#00ff00"), 12.5f, "正常");
        LineChartItem item2 = new LineChartItem(Color.parseColor("#0000ff"), 10.1f, "偏低");
        LineChartItem item3 = new LineChartItem(Color.parseColor("#FF0000"), 20.2f, "偏高");
        LineChartItem item4 = new LineChartItem(Color.parseColor("#FF0000"), 15.1f, "偏高");
        List<LineChartItem> list = new ArrayList<LineChartItem>();
        list.add(item1);
        list.add(item2);
        list.add(item3);
        list.add(item4);
        mLineChart.setItemList(list);

你僅僅用將數據傳遞給LineChart 。剩下的工作就會自己主動完畢了,就是這麽簡單,全然沒有網上那些開源控件用起來那麽復雜吧~~

源代碼 我的github:https://github.com/devilthrone/JKChart
歡迎fork and starO(∩_∩)O
OVER!

自己定義View之Chart圖標系列(1)——點陣圖