我的第三个自定义View(简单折线图绘制)

这次的主角是一个折线图,牛逼的 画图控件已经很多了,
虽然只用过 MPAndroidChart
想着自己写一个学习一下咯
下面是效果


可能录制得有点卡顿

学习路径

变量设置

private Context mContext;

private float mViewHeight, mViewWidth;
private Paint mNormalPointPaint,mSelectedPointPaint, mLinePaint, mBgLinePaint, mTestPaint,
  mBottomTextPaint, mTopTextPaint, mAverageLinePaint, mLifeLongLinePaint, mBottomValuePaint, mBgPaint;//各种画笔

private float mVerticalOffset = dp2px(5); //上下边距
private float mPointWidth = dp2px(4f); //圆点大小(现已修改为图片)
private float mHorizontalOffset = dp2px(15f); //左右边距
private float mValuePaddingOffset;
private boolean mIsHorizontalValue = false; //所有值都相等(是一条水平线)将所有点都画在中间位置

private List<SimpleLineData> mData; // 数据
private List<String> mBottomTexts; // 底部文字集合
private float mBottomTextSize; // 底部文字大小
private int mBottomTextStepSize; // 底部文字 相隔展示间距

private String mTopText; // 顶部中间文字内容
private float mTopTextSize; // 顶部中间文字大小

// 点到点之间的动画相关变量
private int mDrawingLineIndex;
private float mDrawingStopX = -1f, mDrawingStopY = -1f;
private AnimatorSet mAnimatorLine;
private boolean isAnimatingLine;

// 平均线的动画相关变量
private boolean isAnimatingAverageLine;
private AnimatorSet mAnimatorAverageLine;
private float mDrawingStopAverageLineX = -1f;

private String mAverageIconText; //平均线图示文字
private float mAverageIconTextSize; //平均线图示文字大小
private float mAverageValue = -1;

// lifelong 的动画相关变量
private boolean isAnimatingLifelongLine;
private AnimatorSet mAnimatorLifelongLine;
private float mDrawingStopLifelongLineX = -1;

private String mLifeLongIconText; //linflong 图示文字
private float mLifelongIconTextSize;
private float mLifeLongValue = -1;

// 点击点到底部的动画相关变量
private boolean isAnimatingSelectedLine;
private AnimatorSet mAnimatorSelectedLine;
private float mDrawingStopSelectedLineY = -1f;

// 数据值 原点的 图片
private Bitmap mBitmapNormalCircle, mBitmapSelectedCircle;

// 点击位置相关变量
private float mTouchDownX, mTouchDownY;
private float mTouchPadding = dp2px(2.5f);

// 是否绘制的控制
private boolean mIsDrawBottomText = false; //是否绘制底部文字
private boolean mIsDrawAverageLine = true; //是否绘制平均线
private boolean mIsDrawLiflongLine = false; //是否绘制 lifelongprivate boolean mIsDrawVerticalLine = true; //是否绘制背景竖线
private boolean mIsDrawHorizontalLine = false; //是否绘制背景横线
private boolean mIsDrawTopSideLine = true; //是否绘制顶部边线
private boolean mIsDrawBottomSideLine = true; //是否绘制底部边线
private boolean mIsDrawRightSideLine = true; //是否绘制右部边线
private boolean mIsDrawLeftSideLine = true; //是否绘制左部边线
private boolean mIsDrawPointSelectedLine = true; //是否绘制点击时的竖线
private boolean mIsDrawValueTextBottom = true; //是否在底部空间显示点击值的内容

private float mBottomValueTextSize; //底部空间文字大小
private String mBottomValueSuffix = "";
private String mBottomValuePrefix = "";

//各模块颜色配置
private final int DEFAULT_NORMAL_POINT_COLOR = Color.rgb(0,181,255);
private final int DEFAULT_SELECTED_POINT_COLOR = Color.rgb(255,128,97);
private final int DEFAULT_POINT_TO_LINE_COLOR = Color.rgb(0,181,255);
private final int DEFAULT_BACKGROUNG_LINE_COLOR = Color.rgb(194,200,208);
private final int DEFAULT_BOTTOM_TEXT_COLOR = Color.rgb(194,200,208);
private final int DEFAULT_TOP_TEXT_COLOR = Color.rgb(0,181,255);
private final int DEFAULT_AVERAGE_LINE_COLOR = Color.rgb(254,117,117);
private final int DEFAULT_LIFELONG_LINE_COLOR = Color.rgb(175,117,254);
private final int DEFAULT_VIEW_BACKGROUND_COLOR = Color.rgb(250,251,254);
private final int DEFAULT_BOTTOM_VALUE_TEXT_COLOR = Color.rgb(0,181,255);

private int mNormalPointColor = DEFAULT_NORMAL_POINT_COLOR;
private int mSelectedPointColor = DEFAULT_SELECTED_POINT_COLOR;
private int mPointToLineColor = DEFAULT_POINT_TO_LINE_COLOR;
private int mBackgroungLineColor = DEFAULT_BACKGROUNG_LINE_COLOR;
private int mBottomTextColor = DEFAULT_BOTTOM_TEXT_COLOR;
private int mTopTextColor = DEFAULT_TOP_TEXT_COLOR;
private int mAverageLineColor = DEFAULT_AVERAGE_LINE_COLOR;
private int mLifelongLineColor = DEFAULT_LIFELONG_LINE_COLOR;
private int mViewBackgroundColor = DEFAULT_VIEW_BACKGROUND_COLOR;
private int mBottomValueTextColor = DEFAULT_BOTTOM_VALUE_TEXT_COLOR;

private static final int DEFAULT_COLUMN_COUNT = 7;
private int mColumnCount;

综上所述,需要展示以及绘制的东西在上面都已经定义好了。
虽然现在看起来有好多,实际上都是一个个敲出来的,像我这种初学者就不要太心急,别想一口吃撑胖子,一个一个效果实现,然后再去定义变量,然后再开放接口。

类的定义套路

这点就不赘述了,关于构造函数啊,绘制流程啊,差不多都是 万变不离其宗。
我也说不出什么花来。
主要讲讲 draw 的思路好了。

绘制

绘制背景线

在画背景线之前,我做了一个操作

mValuePaddingOffset = (getHeight() - (mIsDrawBottomText ? mBottomTextSize : 0))*0.2f; //把上下两部分(20%)区域 不做绘图区域

意思就是之后的画图(主要是点和线的部分,我都限定在了 除去上下20%的剩余空间里)


img_1473920068.96.jpg

就是把最低点与底部的距离和最高点与顶部的距离 空出来(图上箭头所示),用来放一些图示信息。
最低点是 数据中最小的数值所在的点
最高点是 数据中最大的数值所在的点
当最大值与最小值相等时,所有点都画在中间位置

开始画背景线

private void drawBackgroundLine(Canvas canvas) {
        //画背景线


        for (int i = 0 ; i < mColumnCount; i++) {
            float verticalStartX = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset;
            if(mIsDrawVerticalLine) {
                mIsDrawRightSideLine = true; //如果需要画竖线,默认需要画最右边的竖线
                mIsDrawLeftSideLine  = true; //如果需要画竖线,默认需要画最左边的竖线
                float verticalStartY = mVerticalOffset;
                float verticalStopX  = verticalStartX;
                float verticalStopY  = getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0);
                canvas.drawLine(verticalStartX, verticalStartY, verticalStopX, verticalStopY, mBgLinePaint);
            }

            if(mIsDrawHorizontalLine) {
                mIsDrawTopSideLine = true;
                mIsDrawBottomSideLine = true;
                float horizontalStartX = mHorizontalOffset;
                float horizontalStartY = ((getHeight() - mVerticalOffset*2 - (mIsDrawBottomText ? mBottomTextSize : 0))/(mColumnCount -1))*i + mVerticalOffset;
                float horizontalStopX  = getWidth() - mHorizontalOffset;
                float horiontalStopY   = horizontalStartY;
                canvas.drawLine(horizontalStartX, horizontalStartY, horizontalStopX, horiontalStopY, mBgLinePaint);
            }

            if(mIsDrawBottomText) {
                if (mBottomTexts != null) {
                    //draw bottom text
                    String bottom_text_str = mBottomTexts.get(i);
//                    float bottom_text_width = mBottomTextPaint.measureText(bottom_text_str);
                    if(i % mBottomTextStepSize == 0) {
                        canvas.drawText(bottom_text_str, verticalStartX, getHeight() - mVerticalOffset, mBottomTextPaint);
                    } else if(i == mColumnCount - 1) {
                        if((i - 1) % mBottomTextStepSize != 0) {
                            canvas.drawText(bottom_text_str, verticalStartX, getHeight() - mVerticalOffset, mBottomTextPaint);
                        }
                    }
                }
            }
        }

        if(mIsDrawRightSideLine) {
            //最后一条竖线
            canvas.drawLine(getWidth() - mHorizontalOffset, mVerticalOffset, getWidth() - mHorizontalOffset, getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), mBgLinePaint);
        }

        if(mIsDrawLeftSideLine) {
            canvas.drawLine(mHorizontalOffset, mVerticalOffset, mHorizontalOffset, getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), mBgLinePaint);
        }

        if(mIsDrawBottomSideLine) {
            canvas.drawLine(mHorizontalOffset, getHeight()- mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), getWidth()- mHorizontalOffset, getHeight()- mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), mBgLinePaint);//底部横线
        }
        if(mIsDrawTopSideLine) {
            canvas.drawLine(mHorizontalOffset, mVerticalOffset, getWidth() - mHorizontalOffset, mVerticalOffset, mBgLinePaint);//顶部横线
        }

    }

先画了竖线,竖线的条数与之前设定的 mColumnCount 相关
然后画横线,横线的位置只要由高度,上下边距,以及是否绘制底部文字相关(有的画要去除这部分的高度哇)
当然了,底部的 bottomText 我也当成是一个背景绘制了

绘制圆点位置

圆点的位置是与数据息息相关的。

private void drawPoint(Canvas canvas) {

        float max_value = getMaxValue();
        float min_value = getMinValue();
        float max_pos_y = getHeight() - mVerticalOffset - mValuePaddingOffset - (mIsDrawBottomText ? mBottomTextSize : 0);
        float min_pos_y = mValuePaddingOffset;
        //画点
        float average_value = 0;
        for (int i = 0 ; i < mData.size(); i++) {
            if(mData.get(i).getValue() < 0) {
                continue;
            }
            average_value += mData.get(i).getValue();
            if(mIsHorizontalValue) {
                //之前的圆点是用画的,现在修改为图片了
//                canvas.drawCircle(((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*i + mHorizontalOffset,
//                        min_pos_y + (max_pos_y - min_pos_y)/2, mPointWidth, mNormalPointPaint);


                float left, top;
                if(mColumnCount > 1) {
                    left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - mBitmapNormalCircle.getWidth()/2;
                } else {
                    left = mHorizontalOffset - mBitmapNormalCircle.getWidth()/2;
                }
                top = (min_pos_y + (max_pos_y - min_pos_y)/2) - mBitmapNormalCircle.getHeight()/2;
                boolean isInTouchArea= false;
                if(mTouchDownX > left - mTouchPadding && mTouchDownX < left + mBitmapNormalCircle.getWidth() + mTouchPadding) {
                    //X 符合要求
                    if(mTouchDownY > top - mTouchPadding && mTouchDownY < top + mBitmapNormalCircle.getHeight() + mTouchPadding) {
                        // Y 符合要求
                        isInTouchArea = true;
                    }
                }
                if(isInTouchArea) {
                    if(mIsDrawPointSelectedLine) {
                        if(mDrawingStopSelectedLineY != getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0)) {
                            if(isAnimatingSelectedLine) {
                                float line_x;
                                if(mColumnCount > 1) {
                                    line_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset;
                                } else {
                                    line_x = mHorizontalOffset;
                                }
                                canvas.drawLine(line_x,
                                        min_pos_y + (max_pos_y - min_pos_y)/2, line_x,
                                        mDrawingStopSelectedLineY, mLinePaint);
                            } else {
                                if(mDrawingStopSelectedLineY == -1) {
                                    startSelectedLineAnimation(min_pos_y + (max_pos_y - min_pos_y)/2,
                                            getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0));
                                }
                            }
                        } else {
                            float line_x;
                            if(mColumnCount > 1) {
                                line_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset;
                            } else {
                                line_x = mHorizontalOffset;
                            }
                            canvas.drawLine(line_x,
                                    min_pos_y + (max_pos_y - min_pos_y)/2, line_x,
                                    getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), mLinePaint);
                            if(mIsDrawBottomText) {
                                if (mBottomTexts != null) {
                                    //draw bottom text
                                    String bottom_text_str = mBottomTexts.get(i);
                                    mBottomTextPaint.setColor(mPointToLineColor);
                                    canvas.drawText(bottom_text_str, line_x, getHeight() - mVerticalOffset, mBottomTextPaint);
                                    mBottomTextPaint.setColor(mBottomTextColor);
                                }
                            }
                        }
                    }

                    if(mColumnCount > 1) {
                        left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - mBitmapSelectedCircle.getWidth()/2;
                    } else {
                        left = mHorizontalOffset - mBitmapSelectedCircle.getWidth()/2;
                    }
                    top = (min_pos_y + (max_pos_y - min_pos_y)/2) - mBitmapSelectedCircle.getHeight()/2;
                    canvas.drawBitmap(mBitmapSelectedCircle, left, top, mNormalPointPaint);

                    if(mIsDrawValueTextBottom) {
                        String bottom_value_text = null;
                        if(TextUtils.isEmpty(mData.get(i).getValue_text())) {
                            bottom_value_text = String.valueOf(mData.get(i).getValue());
                            bottom_value_text = mBottomValuePrefix + bottom_value_text + mBottomValueSuffix;
                        } else {
                            bottom_value_text = mData.get(i).getValue_text();
                        }

                        float bottom_value_text_width = mBottomValuePaint.measureText(bottom_value_text);

                        RectF rectF_bg = new RectF();
                        if(mColumnCount > 1) {
                            rectF_bg.left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - bottom_value_text_width/2;
                        } else {
                            rectF_bg.left = mHorizontalOffset - bottom_value_text_width/2;
                        }

                        if(rectF_bg.left < mHorizontalOffset) {
                            rectF_bg.left = mHorizontalOffset + dp2px(2);
                        }

                        if((rectF_bg.left + bottom_value_text_width) > getWidth() - mHorizontalOffset) {
                            rectF_bg.left = rectF_bg.left - bottom_value_text_width/2 - dp2px(2);
                        }

                        rectF_bg.top = getHeight() - mValuePaddingOffset/2 - mBottomValueTextSize/2 - (mIsDrawBottomText ? mBottomTextSize : 0) ;
                        rectF_bg.right = rectF_bg.left + bottom_value_text_width;
                        rectF_bg.bottom = rectF_bg.top + mBottomTextSize;
                        canvas.drawRect(rectF_bg, mBgPaint);

                        canvas.drawText(bottom_value_text, rectF_bg.left + bottom_value_text_width/2,
                                getHeight() - mValuePaddingOffset/2 - (mIsDrawBottomText ? mBottomTextSize : 0) + mBottomValueTextSize/2 + (mBottomValuePaint.descent() + mBottomValuePaint.ascent() / 2.0f), mBottomValuePaint);
                    }
                } else {
                    canvas.drawBitmap(mBitmapNormalCircle, left, top, mNormalPointPaint);
                }



            } else {
//                canvas.drawCircle(((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*i + mHorizontalOffset,
//                        mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y), mPointWidth, mNormalPointPaint);

                float left, top;
                if(mColumnCount > 1) {
                    left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - mBitmapNormalCircle.getWidth()/2;
                } else {
                    left = mHorizontalOffset - mBitmapNormalCircle.getWidth()/2;
                }
                top = (mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y)) - mBitmapNormalCircle.getHeight()/2;
                boolean isInTouchArea= false;
                if(mTouchDownX > left - mTouchPadding && mTouchDownX < left + mBitmapNormalCircle.getWidth() + mTouchPadding) {
                    //X 符合要求
                    if(mTouchDownY > top - mTouchPadding && mTouchDownY < top + mBitmapNormalCircle.getHeight() + mTouchPadding) {
                        // Y 符合要求
                        isInTouchArea = true;
                    }
                }
                if(isInTouchArea) {
                    if(mIsDrawPointSelectedLine) {
                        if(mDrawingStopSelectedLineY != getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0)) {
                            if(isAnimatingSelectedLine) {
                                float line_x;
                                if(mColumnCount > 1) {
                                    line_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset;
                                } else {
                                    line_x = mHorizontalOffset;
                                }
                                canvas.drawLine(line_x,
                                        mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                        line_x,
                                        mDrawingStopSelectedLineY, mLinePaint);
                            } else {
                                if(mDrawingStopSelectedLineY == -1) {
                                    startSelectedLineAnimation(mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                            getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0));
                                }
                            }
                        } else {
                            float line_x;
                            if(mColumnCount > 1) {
                                line_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset;
                            } else {
                                line_x = mHorizontalOffset;
                            }
                            canvas.drawLine(line_x,
                                    mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                    line_x,
                                    getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), mLinePaint);
                            if(mIsDrawBottomText) {
                                if (mBottomTexts != null) {
                                    //draw bottom text
                                    String bottom_text_str = mBottomTexts.get(i);
                                    mBottomTextPaint.setColor(mPointToLineColor);
                                    canvas.drawText(bottom_text_str, line_x, getHeight() - mVerticalOffset, mBottomTextPaint);
                                    mBottomTextPaint.setColor(mBottomTextColor);
                                }
                            }
                        }
                    }


                    if(mColumnCount > 1) {
                        left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - mBitmapSelectedCircle.getWidth()/2;
                    } else {
                        left = mHorizontalOffset - mBitmapSelectedCircle.getWidth()/2;
                    }
                    top = (mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y)) - mBitmapSelectedCircle.getHeight()/2;
                    canvas.drawBitmap(mBitmapSelectedCircle, left, top, mNormalPointPaint);

                    if(mIsDrawValueTextBottom) {
                        String bottom_value_text = null;
                        if(TextUtils.isEmpty(mData.get(i).getValue_text())) {
                            bottom_value_text = String.valueOf(mData.get(i).getValue());
                            bottom_value_text = mBottomValuePrefix + bottom_value_text + mBottomValueSuffix;
                        } else {
                            bottom_value_text = mData.get(i).getValue_text();
                        }

                        float bottom_value_text_width = mBottomValuePaint.measureText(bottom_value_text);

                        RectF rectF_bg = new RectF();
                        if(mColumnCount > 1) {
                            rectF_bg.left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - bottom_value_text_width/2;
                        } else {
                            rectF_bg.left = mHorizontalOffset - bottom_value_text_width/2;
                        }
                        if(rectF_bg.left < mHorizontalOffset) {
                            rectF_bg.left = mHorizontalOffset + dp2px(2);
                        }
                        if((rectF_bg.left + bottom_value_text_width) > getWidth() - mHorizontalOffset) {
                            rectF_bg.left = rectF_bg.left - bottom_value_text_width/2 - dp2px(2);
                        }

                        rectF_bg.top = getHeight() - mValuePaddingOffset/2 - mBottomValueTextSize/2 - (mIsDrawBottomText ? mBottomTextSize : 0) ;
                        rectF_bg.right = rectF_bg.left + bottom_value_text_width;
                        rectF_bg.bottom = rectF_bg.top + mBottomTextSize;
                        canvas.drawRect(rectF_bg, mBgPaint);

                        canvas.drawText(bottom_value_text, rectF_bg.left + bottom_value_text_width/2,
                                getHeight() - mValuePaddingOffset/2 - (mIsDrawBottomText ? mBottomTextSize : 0) + mBottomValueTextSize/2 + (mBottomValuePaint.descent() + mBottomValuePaint.ascent() / 2.0f), mBottomValuePaint);
                    }
                } else {
                    canvas.drawBitmap(mBitmapNormalCircle, left, top, mNormalPointPaint);
                }

            }


        }
        if(mAverageValue < 0) {
            average_value = average_value/mData.size();
        } else {
            average_value = mAverageValue;
        }

        if(mLifeLongValue < 0) {
            mIsDrawLiflongLine = false;
        }

        if(mIsDrawLiflongLine) { // lifelong 是另外一种平均值,可以不用(我用在多个simpleLine所有的平均值)
            if(mDrawingStopLifelongLineX != getWidth() - mHorizontalOffset) {
                if(mIsHorizontalValue) {
                    if(isAnimatingLifelongLine) {
                        //draw average line
                        canvas.drawLine(mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                                mDrawingStopLifelongLineX, min_pos_y + (max_pos_y - min_pos_y)/2,
                                mLifeLongLinePaint);
                    } else {
                        if(mDrawingStopLifelongLineX  == -1) {
                            startLifelongLineAnimation(mHorizontalOffset, getWidth() - mHorizontalOffset);
                        }
                    }
                } else {
                    if(isAnimatingLifelongLine) {
                        canvas.drawLine(mHorizontalOffset,
                                mValuePaddingOffset + ((max_value-mLifeLongValue)/(max_value-min_value))*(max_pos_y-min_pos_y),
                                mDrawingStopLifelongLineX,
                                mValuePaddingOffset + ((max_value-mLifeLongValue)/(max_value-min_value))*(max_pos_y-min_pos_y),
                                mLifeLongLinePaint);
                    } else {
                        if(mDrawingStopLifelongLineX == -1) {
                            startLifelongLineAnimation(mHorizontalOffset, getWidth() - mHorizontalOffset);
                        }
                    }

                }
            } else {
                if(mIsHorizontalValue) {
                    canvas.drawLine(mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                            getWidth() - mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                            mLifeLongLinePaint);
                } else {
                    canvas.drawLine(mHorizontalOffset,
                            mValuePaddingOffset + ((max_value-mLifeLongValue)/(max_value-min_value))*(max_pos_y-min_pos_y),
                            getWidth() - mHorizontalOffset,
                            mValuePaddingOffset + ((max_value-mLifeLongValue)/(max_value-min_value))*(max_pos_y-min_pos_y),
                            mLifeLongLinePaint);
                }
            }
        }

        if(mIsDrawAverageLine) {
            if(mDrawingStopAverageLineX != getWidth() - mHorizontalOffset) {
                if(mIsHorizontalValue) {
                    if(isAnimatingAverageLine) {
                        //draw average line
                        canvas.drawLine(mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                                mDrawingStopAverageLineX, min_pos_y + (max_pos_y - min_pos_y)/2,
                                mAverageLinePaint);
                    } else {
                        if(mDrawingStopAverageLineX  == -1) {
                            startAverageLineAnimation(mHorizontalOffset, getWidth() - mHorizontalOffset);
                        }
                    }
                } else {
                    if(isAnimatingAverageLine) {
                        canvas.drawLine(mHorizontalOffset,
                                mValuePaddingOffset + ((max_value-average_value)/(max_value-min_value))*(max_pos_y-min_pos_y),
                                mDrawingStopAverageLineX,
                                mValuePaddingOffset + ((max_value-average_value)/(max_value-min_value))*(max_pos_y-min_pos_y),
                                mAverageLinePaint);
                    } else {
                        if(mDrawingStopAverageLineX == -1) {
                            startAverageLineAnimation(mHorizontalOffset, getWidth() - mHorizontalOffset);
                        }
                    }

                }
            } else {
                if(mIsHorizontalValue) {
                    canvas.drawLine(mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                            getWidth() - mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                            mAverageLinePaint);
                } else {
                    canvas.drawLine(mHorizontalOffset,
                            mValuePaddingOffset + ((max_value-average_value)/(max_value-min_value))*(max_pos_y-min_pos_y),
                            getWidth() - mHorizontalOffset,
                            mValuePaddingOffset + ((max_value-average_value)/(max_value-min_value))*(max_pos_y-min_pos_y),
                            mAverageLinePaint);
                }
            }
        }


    }

圆点的位置主要跟数据点相关
原点的 X 跟着 竖线走,原点的 Y 跟着数据百分比走
两个点就能确定一个圆的位置,这里这么多代码,很重要的一个问题就是,我不会高级写法
只会一点点计算位置,包括加上上下左右边上的offset,加上圆点图片的长宽,
之前设计的原点本来是绘制的,后来觉得后续如果要修改这个圆点效果,如果绘制的效果很炫,我不会怎么办,就换成了图片,要换成啥样就啥样,连点击效果就随意换,换个图片就好了,省心省事。

圆点连线绘制
    private void drawPoint2Line(Canvas canvas) {
        float max_value = getMaxValue();
        float min_value = getMinValue();
        float max_pos_y = getHeight() - mVerticalOffset - mValuePaddingOffset - (mIsDrawBottomText ? mBottomTextSize : 0);
        float min_pos_y = mValuePaddingOffset;

        if(max_value == min_value) {
            mIsHorizontalValue = true;
        } else {
            mIsHorizontalValue = false;
        }

        //画连接线 不带动画的全部连接线
//        for (int i = 0 ; i < mData.size(); i++) {
//            if(i < mData.size() - 1) {
//                if(mIsHorizontalValue) {
//                    canvas.drawLine(((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*i + mHorizontalOffset,
//                            min_pos_y + (max_pos_y - min_pos_y)/2,
//                            ((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*(i+1) + mHorizontalOffset,
//                            min_pos_y + (max_pos_y - min_pos_y)/2, mLinePaint);
//                } else {
//                    canvas.drawLine(((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*i + mHorizontalOffset,
//                            mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
//                            ((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*(i+1) + mHorizontalOffset,
//                            mValuePaddingOffset + ((max_value-mData.get(i+1).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y), mLinePaint);
//                }
//
//            }
//        }

        boolean hadDrawed = false;
        for (int k = 0 ; k < mData.size() - 1; k++) {
            if(mData.get(k).getValue() < 0) {
                continue;
            }
            if(k < mDrawingLineIndex) {
                hadDrawed = true;
                float line_start_x;
                if(mColumnCount > 1) {
                    line_start_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*k + mHorizontalOffset;
                } else {
                    line_start_x = mHorizontalOffset;
                }
                if(k == mDrawingLineIndex - 1) {

                    if(isAnimatingLine) {

                        if(mIsHorizontalValue) {
                            canvas.drawLine(line_start_x,
                                    min_pos_y + (max_pos_y - min_pos_y)/2,
                                    mDrawingStopX, mDrawingStopY, mLinePaint);
                        } else {
                            canvas.drawLine(line_start_x,
                                    mValuePaddingOffset + ((max_value-mData.get(k).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                    mDrawingStopX, mDrawingStopY, mLinePaint);
                        }
                    } else {
                        float line_stop_x;
                        if(mColumnCount > 1) {
                            line_stop_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*(mDrawingLineIndex) + mHorizontalOffset;
                        } else {
                            line_stop_x = mHorizontalOffset;
                        }
                        if(mIsHorizontalValue) {
                            startLineToAnimation(line_start_x,
                                    min_pos_y + (max_pos_y - min_pos_y)/2,
                                    line_stop_x,
                                    min_pos_y + (max_pos_y - min_pos_y)/2);
                        } else {
                            startLineToAnimation(line_start_x,
                                    mValuePaddingOffset + ((max_value-mData.get(k).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                    line_stop_x,
                                    mValuePaddingOffset + ((max_value-mData.get(mDrawingLineIndex).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y));
                        }
                    }
                } else {
                    float line_stop_x;
                    if(mColumnCount > 1) {
                        line_stop_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*(k+1) + mHorizontalOffset;
                    } else {
                        line_stop_x = mHorizontalOffset;
                    }
                    if(mIsHorizontalValue) {
                        canvas.drawLine(line_start_x,
                                min_pos_y + (max_pos_y - min_pos_y)/2,
                                line_stop_x,
                                min_pos_y + (max_pos_y - min_pos_y)/2, mLinePaint);
                    } else {
                        canvas.drawLine(line_start_x,
                                mValuePaddingOffset + ((max_value-mData.get(k).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                line_stop_x,
                                mValuePaddingOffset + ((max_value-mData.get(k+1).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y), mLinePaint);
                    }
                }
            }
        }

        if(!hadDrawed) {
            mDrawingLineIndex++;
            invalidate();
        }
    }

圆点连线与圆点相关,也就是也跟数据相关。
我没有根据原点来绘制线条,直接就跟数据挂钩了。
对我来说,主要麻烦的地方就是动画,需要计算每一次变化后点的位置,然后确定最终绘制的点的位置。
这里用到了 ValueAnimator 和 贝塞尔曲线的 公式
ValueAnimator 只要是用于计算 两点之间的 过渡值。
贝塞尔曲线才是 核心。

private void startLineToAnimation(float startX, float startY, final float stopX, final float stopY) {
//        Log.d("simpleLineView", "startAnim --> startX-->" + startX + " | startY->" + startY + " | stopX->" + stopX + " | stopY->" + stopY);
        isAnimatingLine = true;

        ValueAnimator xAnimator = ValueAnimator.ofObject(new LineEvaluator(), startX, stopX);
        xAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animation_value  = (float) animation.getAnimatedValue();
                mDrawingStopX = animation_value;
                if(animation_value == stopX) {
                    isAnimatingLine = false;
                    mDrawingLineIndex++;
                    if(mAnimatorLine != null) {
                        mAnimatorLine.cancel();
                    }
                }
                postInvalidate();
            }
        });

        ValueAnimator yAnimator = ValueAnimator.ofObject(new LineEvaluator(), startY, stopY);
        yAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                float animation_value  = (float) animation.getAnimatedValue();
                mDrawingStopY = animation_value;

            }
        });



        mAnimatorLine.playTogether(xAnimator, yAnimator);
        mAnimatorLine.setDuration(2500/ mColumnCount);
        mAnimatorLine.start();
    }

点与点之间的连线 是同时根据 起点的 XY轴一起变化的。所以动画的过渡值肯定也是 XY一起变化
就拿X 来举例吧

ValueAnimator xAnimator = ValueAnimator.ofObject(new LineEvaluator(), startX, stopX);

自定义的LineEvaluator 主要作用是

通过起始值、结束值以及插值时间点来计算在该时间点的属性值应该是多少。
这个东西是属性动画的核心
具体的可以看看 别人的分析,我功力不足 当数学遇上动画:讲述 ValueAnimator、TypeEvaluator 和 TimeInterpolator 之间的恩恩怨怨 (1)
这里的 lineEvaluator 是用得 贝塞尔曲线一阶公式


private class LineEvaluator implements TypeEvaluator {

        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {
            return (1 - fraction) * (float) startValue + fraction * (float) endValue;
        }
    }
绘制 数据图示

图示我的定位是在右上角
既然有了定位,那就慢慢计算咯

    private void drawValueIcon(Canvas canvas) {
        if(mLifeLongValue < 0) {
            mIsDrawLiflongLine = false;
        }

        float lifelong_text_width = mLifeLongLinePaint.measureText(mLifeLongIconText);
        if(mIsDrawLiflongLine) {

            canvas.drawText(mLifeLongIconText, getWidth() - mHorizontalOffset - lifelong_text_width,
                    mVerticalOffset + mValuePaddingOffset/2 + mLifelongIconTextSize/2 + (mLifeLongLinePaint.descent() + mLifeLongLinePaint.ascent() / 2.0f), mLifeLongLinePaint);//文字居中
            canvas.drawCircle(getWidth() - mHorizontalOffset - lifelong_text_width - mLifelongIconTextSize*2 - dp2px(2), mVerticalOffset + mValuePaddingOffset/2, mLifelongIconTextSize/3, mLifeLongLinePaint);
        }

        float average_text_width = mAverageLinePaint.measureText(mAverageIconText);


//        canvas.drawText(mAverageIconText,
//                getWidth() - mHorizontalOffset - average_text_width,
//                mVerticalOffset + mAverageIconTextSize + mAverageIconTextSize/3 + (mAverageLinePaint.descent() + mAverageLinePaint.ascent() / 2.0f),
//                mAverageLinePaint);//文字居中
        canvas.drawText(mAverageIconText, getWidth() - mHorizontalOffset - average_text_width - (mIsDrawLiflongLine ? (lifelong_text_width + mLifelongIconTextSize + dp2px(2) + dp2px(2)) : 0),
                mVerticalOffset + mValuePaddingOffset/2 + mAverageIconTextSize/2 + (mAverageLinePaint.descent() + mAverageLinePaint.ascent() / 2.0f), mAverageLinePaint);


//        canvas.drawCircle(getWidth() - mHorizontalOffset - average_text_width - mAverageIconTextSize*2 - dp2px(2),
//                mVerticalOffset + mAverageIconTextSize/2 + mAverageIconTextSize/3, mAverageIconTextSize/3, mAverageLinePaint);

        canvas.drawCircle(getWidth() - mHorizontalOffset - average_text_width - (mIsDrawLiflongLine ? (lifelong_text_width + mLifelongIconTextSize + dp2px(2) + dp2px(2)) : 0) - mAverageIconTextSize*2 - dp2px(2),
                mVerticalOffset + mValuePaddingOffset/2, mAverageIconTextSize/3, mAverageLinePaint);

    }

主要是平均线的图示绘制
位置基本是固定死的。没什么好说

顶部文字也是差不多。

加入点击事件响应

目前的简单做法是,使用onTouchEvent获取点击位置,然后在绘制的时候计算点击位置与需要绘制的点的位置 是否在 圆点半径之内 来确定点击到了哪一个点。
这个原点半径之内,如果原点图片过小,可能会导致很难点击到的问题。
所以后面加了一个 mTouchPadding 变量来扩大点击范围
但这样会引起另外一个问题:当两个点相差比较近的时候,会有可能计算都两个点都在点击位置范围内。
还有一个动画的绘制,被点击圆点到 底部的 连线动画。
与圆点间连线 一样原理。

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if(event.getAction() == MotionEvent.ACTION_DOWN) {
            mTouchDownX = event.getX();
            mTouchDownY = event.getY();
            mDrawingStopSelectedLineY = -1;
            invalidate();
        }

        return super.onTouchEvent(event);
    }
开放变量设置

之前画的时候都是写死的。
后面一个个写成变量,将变量设置开放到 调用者。
这样的可塑性就好了很多。
这里没有写自定义属性。

用于展示的数据结构
public class SimpleLineData {

    private int index;

    private float value;

    //与value性质相同,但优先级比value 高,主要用于显示与value相关的 特殊文字组成
    private String value_text;

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    public float getValue() {
        return value;
    }

    public void setValue(float value) {
        this.value = value;
    }

    public String getValue_text() {
        return value_text;
    }

    public void setValue_text(String value_text) {
        this.value_text = value_text;
    }
}
测试用的界面Activity
public class ActivityMain extends Activity {

    private SimpleLineView simple_line_view_week, simple_line_view_month;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_ac_main);

        initViews();
    }

    private void initViews() {
        simple_line_view_week = (SimpleLineView) findViewById(R.id.simple_line_view_week);
        simple_line_view_month = (SimpleLineView) findViewById(R.id.simple_line_view_month);

        initData();
    }

    private void initData() {
        initWeekData();
        initMonthData();
    }

    private void initWeekData() {
        List<String>  bottomTexts = new ArrayList<>();
        bottomTexts.add("Sun");
        bottomTexts.add("Mon");
        bottomTexts.add("Tue");
        bottomTexts.add("Wed");
        bottomTexts.add("Thu");
        bottomTexts.add("Fri");
        bottomTexts.add("Sat");


        List<SimpleLineData> data = new ArrayList<>();
        for(int i = 0 ; i < 7; i++) {
            SimpleLineData item = new SimpleLineData();
            item.setIndex(i);
            item.setValue((int) (Math.random() * 99 + 1));
//            item.setValue_text("什么" + item.getValue() + "什么");

            data.add(item);
        }

        simple_line_view_week.setColumnCount(7); //设置 列数
        simple_line_view_week.setData(data);
        simple_line_view_week.setIsDrawBottomText(true); //设置是否绘制底部文字
        simple_line_view_week.setBottomTextList(bottomTexts); //设置底部文字列表
        simple_line_view_week.setTouchPadding(dp2px(5));
        simple_line_view_week.setTopText("week");
    }

    private void initMonthData() {
        List<String>  bottomTexts = new ArrayList<>();

        List<SimpleLineData> data = new ArrayList<>();
        for(int i = 0 ; i < 31; i++) {
            SimpleLineData item = new SimpleLineData();
            item.setIndex(i);
            item.setValue((int) (Math.random() * 99 + 1));

            data.add(item);

            bottomTexts.add((i+1) + "");
        }

        simple_line_view_month.setColumnCount(31);
        simple_line_view_month.setData(data);
        simple_line_view_month.setIsDrawBottomText(false);
        simple_line_view_month.setBottomTextList(bottomTexts);
        simple_line_view_month.setIsDrawBottomText(true);
        simple_line_view_month.setBottomTextStepSize(6);
        simple_line_view_month.setBottomValueSuffix("%");
        simple_line_view_month.setTopText("month");
    }


    private float dp2px(float dp) {
        final float scale = getResources().getDisplayMetrics().density;
        return dp * scale + 0.5f;
    }


}

整个的学习流程大概就是这样。
由于 绘制的计算都是一次次单独计算的,可能会有很多地方的计算都是重复的。
也没有整理,看起来会费劲些,不过可以直接看出绘制的计算思路,所以就没有改掉。
代码都是学习所用,如果有问题,欢迎指点,定当学习改正。

BTW,中秋快乐!
项目地址:Github
已发布到 [JIMBRAY](这次的主角是一个折线图,牛逼的 画图控件已经很多了,
虽然只用过 MPAndroidChart
想着自己写一个学习一下咯
下面是效果


可能录制得有点卡顿

学习路径

变量设置

private Context mContext;

private float mViewHeight, mViewWidth;
private Paint mNormalPointPaint,mSelectedPointPaint, mLinePaint, mBgLinePaint, mTestPaint,
  mBottomTextPaint, mTopTextPaint, mAverageLinePaint, mLifeLongLinePaint, mBottomValuePaint, mBgPaint;//各种画笔

private float mVerticalOffset = dp2px(5); //上下边距
private float mPointWidth = dp2px(4f); //圆点大小(现已修改为图片)
private float mHorizontalOffset = dp2px(15f); //左右边距
private float mValuePaddingOffset;
private boolean mIsHorizontalValue = false; //所有值都相等(是一条水平线)将所有点都画在中间位置

private List<SimpleLineData> mData; // 数据
private List<String> mBottomTexts; // 底部文字集合
private float mBottomTextSize; // 底部文字大小
private int mBottomTextStepSize; // 底部文字 相隔展示间距

private String mTopText; // 顶部中间文字内容
private float mTopTextSize; // 顶部中间文字大小

// 点到点之间的动画相关变量
private int mDrawingLineIndex;
private float mDrawingStopX = -1f, mDrawingStopY = -1f;
private AnimatorSet mAnimatorLine;
private boolean isAnimatingLine;

// 平均线的动画相关变量
private boolean isAnimatingAverageLine;
private AnimatorSet mAnimatorAverageLine;
private float mDrawingStopAverageLineX = -1f;

private String mAverageIconText; //平均线图示文字
private float mAverageIconTextSize; //平均线图示文字大小
private float mAverageValue = -1;

// lifelong 的动画相关变量
private boolean isAnimatingLifelongLine;
private AnimatorSet mAnimatorLifelongLine;
private float mDrawingStopLifelongLineX = -1;

private String mLifeLongIconText; //linflong 图示文字
private float mLifelongIconTextSize;
private float mLifeLongValue = -1;

// 点击点到底部的动画相关变量
private boolean isAnimatingSelectedLine;
private AnimatorSet mAnimatorSelectedLine;
private float mDrawingStopSelectedLineY = -1f;

// 数据值 原点的 图片
private Bitmap mBitmapNormalCircle, mBitmapSelectedCircle;

// 点击位置相关变量
private float mTouchDownX, mTouchDownY;
private float mTouchPadding = dp2px(2.5f);

// 是否绘制的控制
private boolean mIsDrawBottomText = false; //是否绘制底部文字
private boolean mIsDrawAverageLine = true; //是否绘制平均线
private boolean mIsDrawLiflongLine = false; //是否绘制 lifelongprivate boolean mIsDrawVerticalLine = true; //是否绘制背景竖线
private boolean mIsDrawHorizontalLine = false; //是否绘制背景横线
private boolean mIsDrawTopSideLine = true; //是否绘制顶部边线
private boolean mIsDrawBottomSideLine = true; //是否绘制底部边线
private boolean mIsDrawRightSideLine = true; //是否绘制右部边线
private boolean mIsDrawLeftSideLine = true; //是否绘制左部边线
private boolean mIsDrawPointSelectedLine = true; //是否绘制点击时的竖线
private boolean mIsDrawValueTextBottom = true; //是否在底部空间显示点击值的内容

private float mBottomValueTextSize; //底部空间文字大小
private String mBottomValueSuffix = "";
private String mBottomValuePrefix = "";

//各模块颜色配置
private final int DEFAULT_NORMAL_POINT_COLOR = Color.rgb(0,181,255);
private final int DEFAULT_SELECTED_POINT_COLOR = Color.rgb(255,128,97);
private final int DEFAULT_POINT_TO_LINE_COLOR = Color.rgb(0,181,255);
private final int DEFAULT_BACKGROUNG_LINE_COLOR = Color.rgb(194,200,208);
private final int DEFAULT_BOTTOM_TEXT_COLOR = Color.rgb(194,200,208);
private final int DEFAULT_TOP_TEXT_COLOR = Color.rgb(0,181,255);
private final int DEFAULT_AVERAGE_LINE_COLOR = Color.rgb(254,117,117);
private final int DEFAULT_LIFELONG_LINE_COLOR = Color.rgb(175,117,254);
private final int DEFAULT_VIEW_BACKGROUND_COLOR = Color.rgb(250,251,254);
private final int DEFAULT_BOTTOM_VALUE_TEXT_COLOR = Color.rgb(0,181,255);

private int mNormalPointColor = DEFAULT_NORMAL_POINT_COLOR;
private int mSelectedPointColor = DEFAULT_SELECTED_POINT_COLOR;
private int mPointToLineColor = DEFAULT_POINT_TO_LINE_COLOR;
private int mBackgroungLineColor = DEFAULT_BACKGROUNG_LINE_COLOR;
private int mBottomTextColor = DEFAULT_BOTTOM_TEXT_COLOR;
private int mTopTextColor = DEFAULT_TOP_TEXT_COLOR;
private int mAverageLineColor = DEFAULT_AVERAGE_LINE_COLOR;
private int mLifelongLineColor = DEFAULT_LIFELONG_LINE_COLOR;
private int mViewBackgroundColor = DEFAULT_VIEW_BACKGROUND_COLOR;
private int mBottomValueTextColor = DEFAULT_BOTTOM_VALUE_TEXT_COLOR;

private static final int DEFAULT_COLUMN_COUNT = 7;
private int mColumnCount;

综上所述,需要展示以及绘制的东西在上面都已经定义好了。
虽然现在看起来有好多,实际上都是一个个敲出来的,像我这种初学者就不要太心急,别想一口吃撑胖子,一个一个效果实现,然后再去定义变量,然后再开放接口。

类的定义套路

这点就不赘述了,关于构造函数啊,绘制流程啊,差不多都是 万变不离其宗。
我也说不出什么花来。
主要讲讲 draw 的思路好了。

绘制

绘制背景线

在画背景线之前,我做了一个操作

mValuePaddingOffset = (getHeight() - (mIsDrawBottomText ? mBottomTextSize : 0))*0.2f; //把上下两部分(20%)区域 不做绘图区域

意思就是之后的画图(主要是点和线的部分,我都限定在了 除去上下20%的剩余空间里)


img_1473920068.96.jpg

就是把最低点与底部的距离和最高点与顶部的距离 空出来(图上箭头所示),用来放一些图示信息。
最低点是 数据中最小的数值所在的点
最高点是 数据中最大的数值所在的点
当最大值与最小值相等时,所有点都画在中间位置

开始画背景线

private void drawBackgroundLine(Canvas canvas) {
        //画背景线


        for (int i = 0 ; i < mColumnCount; i++) {
            float verticalStartX = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset;
            if(mIsDrawVerticalLine) {
                mIsDrawRightSideLine = true; //如果需要画竖线,默认需要画最右边的竖线
                mIsDrawLeftSideLine  = true; //如果需要画竖线,默认需要画最左边的竖线
                float verticalStartY = mVerticalOffset;
                float verticalStopX  = verticalStartX;
                float verticalStopY  = getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0);
                canvas.drawLine(verticalStartX, verticalStartY, verticalStopX, verticalStopY, mBgLinePaint);
            }

            if(mIsDrawHorizontalLine) {
                mIsDrawTopSideLine = true;
                mIsDrawBottomSideLine = true;
                float horizontalStartX = mHorizontalOffset;
                float horizontalStartY = ((getHeight() - mVerticalOffset*2 - (mIsDrawBottomText ? mBottomTextSize : 0))/(mColumnCount -1))*i + mVerticalOffset;
                float horizontalStopX  = getWidth() - mHorizontalOffset;
                float horiontalStopY   = horizontalStartY;
                canvas.drawLine(horizontalStartX, horizontalStartY, horizontalStopX, horiontalStopY, mBgLinePaint);
            }

            if(mIsDrawBottomText) {
                if (mBottomTexts != null) {
                    //draw bottom text
                    String bottom_text_str = mBottomTexts.get(i);
//                    float bottom_text_width = mBottomTextPaint.measureText(bottom_text_str);
                    if(i % mBottomTextStepSize == 0) {
                        canvas.drawText(bottom_text_str, verticalStartX, getHeight() - mVerticalOffset, mBottomTextPaint);
                    } else if(i == mColumnCount - 1) {
                        if((i - 1) % mBottomTextStepSize != 0) {
                            canvas.drawText(bottom_text_str, verticalStartX, getHeight() - mVerticalOffset, mBottomTextPaint);
                        }
                    }
                }
            }
        }

        if(mIsDrawRightSideLine) {
            //最后一条竖线
            canvas.drawLine(getWidth() - mHorizontalOffset, mVerticalOffset, getWidth() - mHorizontalOffset, getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), mBgLinePaint);
        }

        if(mIsDrawLeftSideLine) {
            canvas.drawLine(mHorizontalOffset, mVerticalOffset, mHorizontalOffset, getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), mBgLinePaint);
        }

        if(mIsDrawBottomSideLine) {
            canvas.drawLine(mHorizontalOffset, getHeight()- mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), getWidth()- mHorizontalOffset, getHeight()- mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), mBgLinePaint);//底部横线
        }
        if(mIsDrawTopSideLine) {
            canvas.drawLine(mHorizontalOffset, mVerticalOffset, getWidth() - mHorizontalOffset, mVerticalOffset, mBgLinePaint);//顶部横线
        }

    }

先画了竖线,竖线的条数与之前设定的 mColumnCount 相关
然后画横线,横线的位置只要由高度,上下边距,以及是否绘制底部文字相关(有的画要去除这部分的高度哇)
当然了,底部的 bottomText 我也当成是一个背景绘制了

绘制圆点位置

圆点的位置是与数据息息相关的。

private void drawPoint(Canvas canvas) {

        float max_value = getMaxValue();
        float min_value = getMinValue();
        float max_pos_y = getHeight() - mVerticalOffset - mValuePaddingOffset - (mIsDrawBottomText ? mBottomTextSize : 0);
        float min_pos_y = mValuePaddingOffset;
        //画点
        float average_value = 0;
        for (int i = 0 ; i < mData.size(); i++) {
            if(mData.get(i).getValue() < 0) {
                continue;
            }
            average_value += mData.get(i).getValue();
            if(mIsHorizontalValue) {
                //之前的圆点是用画的,现在修改为图片了
//                canvas.drawCircle(((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*i + mHorizontalOffset,
//                        min_pos_y + (max_pos_y - min_pos_y)/2, mPointWidth, mNormalPointPaint);


                float left, top;
                if(mColumnCount > 1) {
                    left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - mBitmapNormalCircle.getWidth()/2;
                } else {
                    left = mHorizontalOffset - mBitmapNormalCircle.getWidth()/2;
                }
                top = (min_pos_y + (max_pos_y - min_pos_y)/2) - mBitmapNormalCircle.getHeight()/2;
                boolean isInTouchArea= false;
                if(mTouchDownX > left - mTouchPadding && mTouchDownX < left + mBitmapNormalCircle.getWidth() + mTouchPadding) {
                    //X 符合要求
                    if(mTouchDownY > top - mTouchPadding && mTouchDownY < top + mBitmapNormalCircle.getHeight() + mTouchPadding) {
                        // Y 符合要求
                        isInTouchArea = true;
                    }
                }
                if(isInTouchArea) {
                    if(mIsDrawPointSelectedLine) {
                        if(mDrawingStopSelectedLineY != getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0)) {
                            if(isAnimatingSelectedLine) {
                                float line_x;
                                if(mColumnCount > 1) {
                                    line_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset;
                                } else {
                                    line_x = mHorizontalOffset;
                                }
                                canvas.drawLine(line_x,
                                        min_pos_y + (max_pos_y - min_pos_y)/2, line_x,
                                        mDrawingStopSelectedLineY, mLinePaint);
                            } else {
                                if(mDrawingStopSelectedLineY == -1) {
                                    startSelectedLineAnimation(min_pos_y + (max_pos_y - min_pos_y)/2,
                                            getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0));
                                }
                            }
                        } else {
                            float line_x;
                            if(mColumnCount > 1) {
                                line_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset;
                            } else {
                                line_x = mHorizontalOffset;
                            }
                            canvas.drawLine(line_x,
                                    min_pos_y + (max_pos_y - min_pos_y)/2, line_x,
                                    getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), mLinePaint);
                            if(mIsDrawBottomText) {
                                if (mBottomTexts != null) {
                                    //draw bottom text
                                    String bottom_text_str = mBottomTexts.get(i);
                                    mBottomTextPaint.setColor(mPointToLineColor);
                                    canvas.drawText(bottom_text_str, line_x, getHeight() - mVerticalOffset, mBottomTextPaint);
                                    mBottomTextPaint.setColor(mBottomTextColor);
                                }
                            }
                        }
                    }

                    if(mColumnCount > 1) {
                        left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - mBitmapSelectedCircle.getWidth()/2;
                    } else {
                        left = mHorizontalOffset - mBitmapSelectedCircle.getWidth()/2;
                    }
                    top = (min_pos_y + (max_pos_y - min_pos_y)/2) - mBitmapSelectedCircle.getHeight()/2;
                    canvas.drawBitmap(mBitmapSelectedCircle, left, top, mNormalPointPaint);

                    if(mIsDrawValueTextBottom) {
                        String bottom_value_text = null;
                        if(TextUtils.isEmpty(mData.get(i).getValue_text())) {
                            bottom_value_text = String.valueOf(mData.get(i).getValue());
                            bottom_value_text = mBottomValuePrefix + bottom_value_text + mBottomValueSuffix;
                        } else {
                            bottom_value_text = mData.get(i).getValue_text();
                        }

                        float bottom_value_text_width = mBottomValuePaint.measureText(bottom_value_text);

                        RectF rectF_bg = new RectF();
                        if(mColumnCount > 1) {
                            rectF_bg.left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - bottom_value_text_width/2;
                        } else {
                            rectF_bg.left = mHorizontalOffset - bottom_value_text_width/2;
                        }

                        if(rectF_bg.left < mHorizontalOffset) {
                            rectF_bg.left = mHorizontalOffset + dp2px(2);
                        }

                        if((rectF_bg.left + bottom_value_text_width) > getWidth() - mHorizontalOffset) {
                            rectF_bg.left = rectF_bg.left - bottom_value_text_width/2 - dp2px(2);
                        }

                        rectF_bg.top = getHeight() - mValuePaddingOffset/2 - mBottomValueTextSize/2 - (mIsDrawBottomText ? mBottomTextSize : 0) ;
                        rectF_bg.right = rectF_bg.left + bottom_value_text_width;
                        rectF_bg.bottom = rectF_bg.top + mBottomTextSize;
                        canvas.drawRect(rectF_bg, mBgPaint);

                        canvas.drawText(bottom_value_text, rectF_bg.left + bottom_value_text_width/2,
                                getHeight() - mValuePaddingOffset/2 - (mIsDrawBottomText ? mBottomTextSize : 0) + mBottomValueTextSize/2 + (mBottomValuePaint.descent() + mBottomValuePaint.ascent() / 2.0f), mBottomValuePaint);
                    }
                } else {
                    canvas.drawBitmap(mBitmapNormalCircle, left, top, mNormalPointPaint);
                }



            } else {
//                canvas.drawCircle(((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*i + mHorizontalOffset,
//                        mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y), mPointWidth, mNormalPointPaint);

                float left, top;
                if(mColumnCount > 1) {
                    left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - mBitmapNormalCircle.getWidth()/2;
                } else {
                    left = mHorizontalOffset - mBitmapNormalCircle.getWidth()/2;
                }
                top = (mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y)) - mBitmapNormalCircle.getHeight()/2;
                boolean isInTouchArea= false;
                if(mTouchDownX > left - mTouchPadding && mTouchDownX < left + mBitmapNormalCircle.getWidth() + mTouchPadding) {
                    //X 符合要求
                    if(mTouchDownY > top - mTouchPadding && mTouchDownY < top + mBitmapNormalCircle.getHeight() + mTouchPadding) {
                        // Y 符合要求
                        isInTouchArea = true;
                    }
                }
                if(isInTouchArea) {
                    if(mIsDrawPointSelectedLine) {
                        if(mDrawingStopSelectedLineY != getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0)) {
                            if(isAnimatingSelectedLine) {
                                float line_x;
                                if(mColumnCount > 1) {
                                    line_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset;
                                } else {
                                    line_x = mHorizontalOffset;
                                }
                                canvas.drawLine(line_x,
                                        mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                        line_x,
                                        mDrawingStopSelectedLineY, mLinePaint);
                            } else {
                                if(mDrawingStopSelectedLineY == -1) {
                                    startSelectedLineAnimation(mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                            getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0));
                                }
                            }
                        } else {
                            float line_x;
                            if(mColumnCount > 1) {
                                line_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset;
                            } else {
                                line_x = mHorizontalOffset;
                            }
                            canvas.drawLine(line_x,
                                    mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                    line_x,
                                    getHeight() - mVerticalOffset - (mIsDrawBottomText ? mBottomTextSize : 0), mLinePaint);
                            if(mIsDrawBottomText) {
                                if (mBottomTexts != null) {
                                    //draw bottom text
                                    String bottom_text_str = mBottomTexts.get(i);
                                    mBottomTextPaint.setColor(mPointToLineColor);
                                    canvas.drawText(bottom_text_str, line_x, getHeight() - mVerticalOffset, mBottomTextPaint);
                                    mBottomTextPaint.setColor(mBottomTextColor);
                                }
                            }
                        }
                    }


                    if(mColumnCount > 1) {
                        left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - mBitmapSelectedCircle.getWidth()/2;
                    } else {
                        left = mHorizontalOffset - mBitmapSelectedCircle.getWidth()/2;
                    }
                    top = (mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y)) - mBitmapSelectedCircle.getHeight()/2;
                    canvas.drawBitmap(mBitmapSelectedCircle, left, top, mNormalPointPaint);

                    if(mIsDrawValueTextBottom) {
                        String bottom_value_text = null;
                        if(TextUtils.isEmpty(mData.get(i).getValue_text())) {
                            bottom_value_text = String.valueOf(mData.get(i).getValue());
                            bottom_value_text = mBottomValuePrefix + bottom_value_text + mBottomValueSuffix;
                        } else {
                            bottom_value_text = mData.get(i).getValue_text();
                        }

                        float bottom_value_text_width = mBottomValuePaint.measureText(bottom_value_text);

                        RectF rectF_bg = new RectF();
                        if(mColumnCount > 1) {
                            rectF_bg.left = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*i + mHorizontalOffset - bottom_value_text_width/2;
                        } else {
                            rectF_bg.left = mHorizontalOffset - bottom_value_text_width/2;
                        }
                        if(rectF_bg.left < mHorizontalOffset) {
                            rectF_bg.left = mHorizontalOffset + dp2px(2);
                        }
                        if((rectF_bg.left + bottom_value_text_width) > getWidth() - mHorizontalOffset) {
                            rectF_bg.left = rectF_bg.left - bottom_value_text_width/2 - dp2px(2);
                        }

                        rectF_bg.top = getHeight() - mValuePaddingOffset/2 - mBottomValueTextSize/2 - (mIsDrawBottomText ? mBottomTextSize : 0) ;
                        rectF_bg.right = rectF_bg.left + bottom_value_text_width;
                        rectF_bg.bottom = rectF_bg.top + mBottomTextSize;
                        canvas.drawRect(rectF_bg, mBgPaint);

                        canvas.drawText(bottom_value_text, rectF_bg.left + bottom_value_text_width/2,
                                getHeight() - mValuePaddingOffset/2 - (mIsDrawBottomText ? mBottomTextSize : 0) + mBottomValueTextSize/2 + (mBottomValuePaint.descent() + mBottomValuePaint.ascent() / 2.0f), mBottomValuePaint);
                    }
                } else {
                    canvas.drawBitmap(mBitmapNormalCircle, left, top, mNormalPointPaint);
                }

            }


        }
        if(mAverageValue < 0) {
            average_value = average_value/mData.size();
        } else {
            average_value = mAverageValue;
        }

        if(mLifeLongValue < 0) {
            mIsDrawLiflongLine = false;
        }

        if(mIsDrawLiflongLine) { // lifelong 是另外一种平均值,可以不用(我用在多个simpleLine所有的平均值)
            if(mDrawingStopLifelongLineX != getWidth() - mHorizontalOffset) {
                if(mIsHorizontalValue) {
                    if(isAnimatingLifelongLine) {
                        //draw average line
                        canvas.drawLine(mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                                mDrawingStopLifelongLineX, min_pos_y + (max_pos_y - min_pos_y)/2,
                                mLifeLongLinePaint);
                    } else {
                        if(mDrawingStopLifelongLineX  == -1) {
                            startLifelongLineAnimation(mHorizontalOffset, getWidth() - mHorizontalOffset);
                        }
                    }
                } else {
                    if(isAnimatingLifelongLine) {
                        canvas.drawLine(mHorizontalOffset,
                                mValuePaddingOffset + ((max_value-mLifeLongValue)/(max_value-min_value))*(max_pos_y-min_pos_y),
                                mDrawingStopLifelongLineX,
                                mValuePaddingOffset + ((max_value-mLifeLongValue)/(max_value-min_value))*(max_pos_y-min_pos_y),
                                mLifeLongLinePaint);
                    } else {
                        if(mDrawingStopLifelongLineX == -1) {
                            startLifelongLineAnimation(mHorizontalOffset, getWidth() - mHorizontalOffset);
                        }
                    }

                }
            } else {
                if(mIsHorizontalValue) {
                    canvas.drawLine(mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                            getWidth() - mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                            mLifeLongLinePaint);
                } else {
                    canvas.drawLine(mHorizontalOffset,
                            mValuePaddingOffset + ((max_value-mLifeLongValue)/(max_value-min_value))*(max_pos_y-min_pos_y),
                            getWidth() - mHorizontalOffset,
                            mValuePaddingOffset + ((max_value-mLifeLongValue)/(max_value-min_value))*(max_pos_y-min_pos_y),
                            mLifeLongLinePaint);
                }
            }
        }

        if(mIsDrawAverageLine) {
            if(mDrawingStopAverageLineX != getWidth() - mHorizontalOffset) {
                if(mIsHorizontalValue) {
                    if(isAnimatingAverageLine) {
                        //draw average line
                        canvas.drawLine(mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                                mDrawingStopAverageLineX, min_pos_y + (max_pos_y - min_pos_y)/2,
                                mAverageLinePaint);
                    } else {
                        if(mDrawingStopAverageLineX  == -1) {
                            startAverageLineAnimation(mHorizontalOffset, getWidth() - mHorizontalOffset);
                        }
                    }
                } else {
                    if(isAnimatingAverageLine) {
                        canvas.drawLine(mHorizontalOffset,
                                mValuePaddingOffset + ((max_value-average_value)/(max_value-min_value))*(max_pos_y-min_pos_y),
                                mDrawingStopAverageLineX,
                                mValuePaddingOffset + ((max_value-average_value)/(max_value-min_value))*(max_pos_y-min_pos_y),
                                mAverageLinePaint);
                    } else {
                        if(mDrawingStopAverageLineX == -1) {
                            startAverageLineAnimation(mHorizontalOffset, getWidth() - mHorizontalOffset);
                        }
                    }

                }
            } else {
                if(mIsHorizontalValue) {
                    canvas.drawLine(mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                            getWidth() - mHorizontalOffset, min_pos_y + (max_pos_y - min_pos_y)/2,
                            mAverageLinePaint);
                } else {
                    canvas.drawLine(mHorizontalOffset,
                            mValuePaddingOffset + ((max_value-average_value)/(max_value-min_value))*(max_pos_y-min_pos_y),
                            getWidth() - mHorizontalOffset,
                            mValuePaddingOffset + ((max_value-average_value)/(max_value-min_value))*(max_pos_y-min_pos_y),
                            mAverageLinePaint);
                }
            }
        }


    }

圆点的位置主要跟数据点相关
原点的 X 跟着 竖线走,原点的 Y 跟着数据百分比走
两个点就能确定一个圆的位置,这里这么多代码,很重要的一个问题就是,我不会高级写法
只会一点点计算位置,包括加上上下左右边上的offset,加上圆点图片的长宽,
之前设计的原点本来是绘制的,后来觉得后续如果要修改这个圆点效果,如果绘制的效果很炫,我不会怎么办,就换成了图片,要换成啥样就啥样,连点击效果就随意换,换个图片就好了,省心省事。

圆点连线绘制
    private void drawPoint2Line(Canvas canvas) {
        float max_value = getMaxValue();
        float min_value = getMinValue();
        float max_pos_y = getHeight() - mVerticalOffset - mValuePaddingOffset - (mIsDrawBottomText ? mBottomTextSize : 0);
        float min_pos_y = mValuePaddingOffset;

        if(max_value == min_value) {
            mIsHorizontalValue = true;
        } else {
            mIsHorizontalValue = false;
        }

        //画连接线 不带动画的全部连接线
//        for (int i = 0 ; i < mData.size(); i++) {
//            if(i < mData.size() - 1) {
//                if(mIsHorizontalValue) {
//                    canvas.drawLine(((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*i + mHorizontalOffset,
//                            min_pos_y + (max_pos_y - min_pos_y)/2,
//                            ((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*(i+1) + mHorizontalOffset,
//                            min_pos_y + (max_pos_y - min_pos_y)/2, mLinePaint);
//                } else {
//                    canvas.drawLine(((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*i + mHorizontalOffset,
//                            mValuePaddingOffset + ((max_value-mData.get(i).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
//                            ((getWidth()- mHorizontalOffset *2)/(mData.size() - 1))*(i+1) + mHorizontalOffset,
//                            mValuePaddingOffset + ((max_value-mData.get(i+1).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y), mLinePaint);
//                }
//
//            }
//        }

        boolean hadDrawed = false;
        for (int k = 0 ; k < mData.size() - 1; k++) {
            if(mData.get(k).getValue() < 0) {
                continue;
            }
            if(k < mDrawingLineIndex) {
                hadDrawed = true;
                float line_start_x;
                if(mColumnCount > 1) {
                    line_start_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*k + mHorizontalOffset;
                } else {
                    line_start_x = mHorizontalOffset;
                }
                if(k == mDrawingLineIndex - 1) {

                    if(isAnimatingLine) {

                        if(mIsHorizontalValue) {
                            canvas.drawLine(line_start_x,
                                    min_pos_y + (max_pos_y - min_pos_y)/2,
                                    mDrawingStopX, mDrawingStopY, mLinePaint);
                        } else {
                            canvas.drawLine(line_start_x,
                                    mValuePaddingOffset + ((max_value-mData.get(k).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                    mDrawingStopX, mDrawingStopY, mLinePaint);
                        }
                    } else {
                        float line_stop_x;
                        if(mColumnCount > 1) {
                            line_stop_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*(mDrawingLineIndex) + mHorizontalOffset;
                        } else {
                            line_stop_x = mHorizontalOffset;
                        }
                        if(mIsHorizontalValue) {
                            startLineToAnimation(line_start_x,
                                    min_pos_y + (max_pos_y - min_pos_y)/2,
                                    line_stop_x,
                                    min_pos_y + (max_pos_y - min_pos_y)/2);
                        } else {
                            startLineToAnimation(line_start_x,
                                    mValuePaddingOffset + ((max_value-mData.get(k).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                    line_stop_x,
                                    mValuePaddingOffset + ((max_value-mData.get(mDrawingLineIndex).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y));
                        }
                    }
                } else {
                    float line_stop_x;
                    if(mColumnCount > 1) {
                        line_stop_x = ((getWidth()- mHorizontalOffset *2)/(mColumnCount - 1))*(k+1) + mHorizontalOffset;
                    } else {
                        line_stop_x = mHorizontalOffset;
                    }
                    if(mIsHorizontalValue) {
                        canvas.drawLine(line_start_x,
                                min_pos_y + (max_pos_y - min_pos_y)/2,
                                line_stop_x,
                                min_pos_y + (max_pos_y - min_pos_y)/2, mLinePaint);
                    } else {
                        canvas.drawLine(line_start_x,
                                mValuePaddingOffset + ((max_value-mData.get(k).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y),
                                line_stop_x,
                                mValuePaddingOffset + ((max_value-mData.get(k+1).getValue())/(max_value-min_value))*(max_pos_y-min_pos_y), mLinePaint);
                    }
                }
            }
        }

        if(!hadDrawed) {
            mDrawingLineIndex++;
            invalidate();
        }
    }

圆点连线与圆点相关,也就是也跟数据相关。
我没有根据原点来绘制线条,直接就跟数据挂钩了。
对我来说,主要麻烦的地方就是动画,需要计算每一次变化后点的位置,然后确定最终绘制的点的位置。
这里用到了 ValueAnimator 和 贝塞尔曲线的 公式
ValueAnimator 只要是用于计算 两点之间的 过渡值。
贝塞尔曲线才是 核心。

private void startLineToAnimation(float startX, float startY, final float stopX, final float stopY) {
//        Log.d("simpleLineView", "startAnim --> startX-->" + startX + " | startY->" + startY + " | stopX->" + stopX + " | stopY->" + stopY);
        isAnimatingLine = true;

        ValueAnimator xAnimator = ValueAnimator.ofObject(new LineEvaluator(), startX, stopX);
        xAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float animation_value  = (float) animation.getAnimatedValue();
                mDrawingStopX = animation_value;
                if(animation_value == stopX) {
                    isAnimatingLine = false;
                    mDrawingLineIndex++;
                    if(mAnimatorLine != null) {
                        mAnimatorLine.cancel();
                    }
                }
                postInvalidate();
            }
        });

        ValueAnimator yAnimator = ValueAnimator.ofObject(new LineEvaluator(), startY, stopY);
        yAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                float animation_value  = (float) animation.getAnimatedValue();
                mDrawingStopY = animation_value;

            }
        });



        mAnimatorLine.playTogether(xAnimator, yAnimator);
        mAnimatorLine.setDuration(2500/ mColumnCount);
        mAnimatorLine.start();
    }

点与点之间的连线 是同时根据 起点的 XY轴一起变化的。所以动画的过渡值肯定也是 XY一起变化
就拿X 来举例吧

ValueAnimator xAnimator = ValueAnimator.ofObject(new LineEvaluator(), startX, stopX);

自定义的LineEvaluator 主要作用是

通过起始值、结束值以及插值时间点来计算在该时间点的属性值应该是多少。
这个东西是属性动画的核心
具体的可以看看 别人的分析,我功力不足 当数学遇上动画:讲述 ValueAnimator、TypeEvaluator 和 TimeInterpolator 之间的恩恩怨怨 (1)
这里的 lineEvaluator 是用得 贝塞尔曲线一阶公式


private class LineEvaluator implements TypeEvaluator {

        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {
            return (1 - fraction) * (float) startValue + fraction * (float) endValue;
        }
    }
绘制 数据图示

图示我的定位是在右上角
既然有了定位,那就慢慢计算咯

    private void drawValueIcon(Canvas canvas) {
        if(mLifeLongValue < 0) {
            mIsDrawLiflongLine = false;
        }

        float lifelong_text_width = mLifeLongLinePaint.measureText(mLifeLongIconText);
        if(mIsDrawLiflongLine) {

            canvas.drawText(mLifeLongIconText, getWidth() - mHorizontalOffset - lifelong_text_width,
                    mVerticalOffset + mValuePaddingOffset/2 + mLifelongIconTextSize/2 + (mLifeLongLinePaint.descent() + mLifeLongLinePaint.ascent() / 2.0f), mLifeLongLinePaint);//文字居中
            canvas.drawCircle(getWidth() - mHorizontalOffset - lifelong_text_width - mLifelongIconTextSize*2 - dp2px(2), mVerticalOffset + mValuePaddingOffset/2, mLifelongIconTextSize/3, mLifeLongLinePaint);
        }

        float average_text_width = mAverageLinePaint.measureText(mAverageIconText);


//        canvas.drawText(mAverageIconText,
//                getWidth() - mHorizontalOffset - average_text_width,
//                mVerticalOffset + mAverageIconTextSize + mAverageIconTextSize/3 + (mAverageLinePaint.descent() + mAverageLinePaint.ascent() / 2.0f),
//                mAverageLinePaint);//文字居中
        canvas.drawText(mAverageIconText, getWidth() - mHorizontalOffset - average_text_width - (mIsDrawLiflongLine ? (lifelong_text_width + mLifelongIconTextSize + dp2px(2) + dp2px(2)) : 0),
                mVerticalOffset + mValuePaddingOffset/2 + mAverageIconTextSize/2 + (mAverageLinePaint.descent() + mAverageLinePaint.ascent() / 2.0f), mAverageLinePaint);


//        canvas.drawCircle(getWidth() - mHorizontalOffset - average_text_width - mAverageIconTextSize*2 - dp2px(2),
//                mVerticalOffset + mAverageIconTextSize/2 + mAverageIconTextSize/3, mAverageIconTextSize/3, mAverageLinePaint);

        canvas.drawCircle(getWidth() - mHorizontalOffset - average_text_width - (mIsDrawLiflongLine ? (lifelong_text_width + mLifelongIconTextSize + dp2px(2) + dp2px(2)) : 0) - mAverageIconTextSize*2 - dp2px(2),
                mVerticalOffset + mValuePaddingOffset/2, mAverageIconTextSize/3, mAverageLinePaint);

    }

主要是平均线的图示绘制
位置基本是固定死的。没什么好说

顶部文字也是差不多。

加入点击事件响应

目前的简单做法是,使用onTouchEvent获取点击位置,然后在绘制的时候计算点击位置与需要绘制的点的位置 是否在 圆点半径之内 来确定点击到了哪一个点。
这个原点半径之内,如果原点图片过小,可能会导致很难点击到的问题。
所以后面加了一个 mTouchPadding 变量来扩大点击范围
但这样会引起另外一个问题:当两个点相差比较近的时候,会有可能计算都两个点都在点击位置范围内。
还有一个动画的绘制,被点击圆点到 底部的 连线动画。
与圆点间连线 一样原理。

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        if(event.getAction() == MotionEvent.ACTION_DOWN) {
            mTouchDownX = event.getX();
            mTouchDownY = event.getY();
            mDrawingStopSelectedLineY = -1;
            invalidate();
        }

        return super.onTouchEvent(event);
    }
开放变量设置

之前画的时候都是写死的。
后面一个个写成变量,将变量设置开放到 调用者。
这样的可塑性就好了很多。
这里没有写自定义属性。

用于展示的数据结构
public class SimpleLineData {

    private int index;

    private float value;

    //与value性质相同,但优先级比value 高,主要用于显示与value相关的 特殊文字组成
    private String value_text;

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }

    public float getValue() {
        return value;
    }

    public void setValue(float value) {
        this.value = value;
    }

    public String getValue_text() {
        return value_text;
    }

    public void setValue_text(String value_text) {
        this.value_text = value_text;
    }
}
测试用的界面Activity
public class ActivityMain extends Activity {

    private SimpleLineView simple_line_view_week, simple_line_view_month;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_ac_main);

        initViews();
    }

    private void initViews() {
        simple_line_view_week = (SimpleLineView) findViewById(R.id.simple_line_view_week);
        simple_line_view_month = (SimpleLineView) findViewById(R.id.simple_line_view_month);

        initData();
    }

    private void initData() {
        initWeekData();
        initMonthData();
    }

    private void initWeekData() {
        List<String>  bottomTexts = new ArrayList<>();
        bottomTexts.add("Sun");
        bottomTexts.add("Mon");
        bottomTexts.add("Tue");
        bottomTexts.add("Wed");
        bottomTexts.add("Thu");
        bottomTexts.add("Fri");
        bottomTexts.add("Sat");


        List<SimpleLineData> data = new ArrayList<>();
        for(int i = 0 ; i < 7; i++) {
            SimpleLineData item = new SimpleLineData();
            item.setIndex(i);
            item.setValue((int) (Math.random() * 99 + 1));
//            item.setValue_text("什么" + item.getValue() + "什么");

            data.add(item);
        }

        simple_line_view_week.setColumnCount(7); //设置 列数
        simple_line_view_week.setData(data);
        simple_line_view_week.setIsDrawBottomText(true); //设置是否绘制底部文字
        simple_line_view_week.setBottomTextList(bottomTexts); //设置底部文字列表
        simple_line_view_week.setTouchPadding(dp2px(5));
        simple_line_view_week.setTopText("week");
    }

    private void initMonthData() {
        List<String>  bottomTexts = new ArrayList<>();

        List<SimpleLineData> data = new ArrayList<>();
        for(int i = 0 ; i < 31; i++) {
            SimpleLineData item = new SimpleLineData();
            item.setIndex(i);
            item.setValue((int) (Math.random() * 99 + 1));

            data.add(item);

            bottomTexts.add((i+1) + "");
        }

        simple_line_view_month.setColumnCount(31);
        simple_line_view_month.setData(data);
        simple_line_view_month.setIsDrawBottomText(false);
        simple_line_view_month.setBottomTextList(bottomTexts);
        simple_line_view_month.setIsDrawBottomText(true);
        simple_line_view_month.setBottomTextStepSize(6);
        simple_line_view_month.setBottomValueSuffix("%");
        simple_line_view_month.setTopText("month");
    }


    private float dp2px(float dp) {
        final float scale = getResources().getDisplayMetrics().density;
        return dp * scale + 0.5f;
    }


}

整个的学习流程大概就是这样。
由于 绘制的计算都是一次次单独计算的,可能会有很多地方的计算都是重复的。
也没有整理,看起来会费劲些,不过可以直接看出绘制的计算思路,所以就没有改掉。
代码都是学习所用,如果有问题,欢迎指点,定当学习改正。

BTW,中秋快乐!
已发布到:JIMBRAY
项目地址:Github)

推荐阅读更多精彩内容