老规矩,先上实现的效果图
github地址
https://github.com/Alan222/MyChartView
这些基本的资源文件写了吧,以免最后忘了加
dimens文件
activity_mainname="activity_horizontal_margin">16dp name="activity_vertical_margin">16dp name="dp20">20dp name="dp3">3dp name="dp6">6dp name="textSize">10dp
android:id="@+id/chart"
android:layout_width="match_parent"
android:layout_height="200dp"
/>
MyChartView 需求分析
需要画的东西
1.x,y轴坐标的线,箭头
2.x,y轴的标题,刻度下的字
3.折线和点,渐变背景色
分析完需求我们先不要急着去实现这些功能,先搭简单的模板;
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); initData(); //画坐标轴之前我们需要先初始化一些数据 drawAxes(canvas); // 画坐标轴 drawText(canvas);// 画文字 drawLine(canvas);// 画折线 } private void initData() { } private void drawAxes(Canvas canvas) { } private void drawLine(Canvas canvas) { } private void drawText(Canvas canvas) { }
好了,画完了
开个玩笑我们先来实现第一步,画线
分析一下画线需要的数据
1.1.控件的宽高(用来确定x,y轴的宽高)
1.2.画线必须要有画笔
1.3.坐标起始点
1.4.x轴两条线之间的间距
1.5.y轴两条线之间的间距
分析完需求我们用代码去实现,
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { //1.1.控件的宽高(用来确定x,y轴的宽高) mWidth = getWidth(); mHeight = getHeight(); } }
private void initData() { //1.2.初始化画笔 mPaint = new Paint(); mPaint.setStyle(Paint.Style.STROKE); mPaint.setAntiAlias(true);//去锯齿 mPaint.setColor(mPaintColor);//颜色 mPaint.setTextSize(mTextSize); //1.3.坐标起始点 xPoint = (int) getContext().getResources().getDimension(R.dimen.dp20); yTopPoint = (int) getContext().getResources().getDimension(R.dimen.dp20); yPoint = mHeight - yTopPoint; //1.4.x轴两条线之间的间距 //1.5.y轴两条线之间的间距 dp3 = (int) getContext().getResources().getDimension(R.dimen.dp3); //箭头的偏移量 dp6 = (int) getContext().getResources().getDimension(R.dimen.dp6); //箭头的偏移量 yItemDistance = (yPoint - (yTopPoint + mTextSize)) / 5;//y轴字段坐标的间距 5代表除起始坐标外有5条线,暂时先死,稍后会根据y轴的数据来 xItemDistance = (mWidth - xPoint) / 7; //x轴字段坐标的间距 7代表除起始坐标外有7条线,暂时先死,稍后会根据y轴的数据来 }
有了这些数据我们就可以开始画线了
private void drawAxes(Canvas canvas) { //画起始坐标轴和箭头 canvas.drawLine(xPoint, yPoint, mWidth - xPoint, yPoint, mPaint); //x轴起始坐标线 canvas.drawLine(mWidth - xPoint, yPoint, mWidth - xPoint - dp6, yPoint + dp3, mPaint); //向右箭头 canvas.drawLine(mWidth - xPoint, yPoint, mWidth - xPoint - dp6, yPoint - dp3, mPaint); canvas.drawLine(xPoint, yPoint, xPoint, yTopPoint, mPaint); //y起始轴线 canvas.drawLine(xPoint, yTopPoint, xPoint - dp3, yTopPoint + dp6, mPaint); //向右箭头 canvas.drawLine(xPoint, yTopPoint, xPoint + dp3, yTopPoint + dp6, mPaint); //画横着的线 int yTextPoint; //y轴字段的坐标 for (int i = 0; i < 5; i++) { yTextPoint = yTopPoint + mTextSize + yItemDistance * i; canvas.drawLine(xPoint, yTextPoint, mWidth - xPoint, yTextPoint, mPaint); //x轴线 } //画竖着的线 int xTextPoint; //x轴字段的坐标 for (int i = 1; i < 7; i++) { xTextPoint = xPoint + xItemDistance * i; canvas.drawLine(xTextPoint, yPoint, xTextPoint, yTopPoint + mTextSize, mPaint); //y轴线这里的线比起始Y轴短,留出位置画title } }
画完背景线,我们先测试一下
附上成员变量
PS:有值的成员变量暂时先写死,实现需求以后用attr实现
private int mTextSize = (int) getResources().getDimension(R.dimen.textSize); private int mPaintColor = Color.RED; private int mWidth;//控件的宽 private int mHeight;//控件的高 private Paint mPaint; private int xPoint; //原点X轴坐标 private int yPoint; //原点Y轴坐标 private int yTopPoint;//y轴顶点坐标 private int dp3; private int dp6; private int yItemDistance; private int xItemDistance;
ok,接下来开始画x,y轴的字段和标题
我们需要
2.1.x轴的数据
2.2.y轴的数据
2.3.x轴的title
2.4.y轴的title
2.5.数据的宽和高
分析完继续撸代码
private void initData() { //2.1.x轴的数据 //2.2.y轴的数据 // FIXME: 2017/4/17 暂时先写死 xData = new ArrayList<>(); xData.add("4-11"); xData.add("4-12"); xData.add("4-13"); xData.add("4-14"); xData.add("4-15"); xData.add("4-16"); xData.add("4-17"); yData = new ArrayList<>(); yData.add(0); yData.add(10); yData.add(20); yData.add(30); yData.add(40); yData.add(50); yData.add(60); mPaint = new Paint(); ...... }
//2.3.x轴的title public void setxTitle(String xTitle) { this.xTitle = xTitle; } //2.4.y轴的title public void setyTitle(String yTitle) { this.yTitle = yTitle; }
//2.5.测量文字的宽,文字的高为textsize public int measureTextWidth(String text) { return (int) mPaint.measureText(text); }
这里写个成员变量吧 不然代码要来亲戚了
private String xTitle = "近七日"; private String yTitle = "增长率/%"; private ArrayListxData = new ArrayList<>(); private ArrayList yData = new ArrayList<>();
做完准备工作可以开始画刻度了
private void drawText(Canvas canvas) { //画x轴的title int xTitleWidth = measureTextWidth(xTitle); canvas.drawText(xTitle, mWidth - xPoint - xTitleWidth / 2, yPoint + dp3 + mTextSize, mPaint); //画y轴的title canvas.drawText(yTitle, xPoint + dp6, yTopPoint + mTextSize / 2, mPaint); //画x轴刻度 for (int i = 0; i < xData.size(); i++) { int dataWidth = measureTextWidth(xData.get(i)); canvas.drawText(xData.get(i), xPoint + xItemDistance * i - dataWidth / 2, yPoint + dp3 + mTextSize, mPaint); } //画y轴刻度 for (int i = 0; i < yData.size(); i++) { int dataWidth = measureTextWidth(yData.get(i) + ""); canvas.drawText(yData.get(i) + "", xPoint - dataWidth - dp3, yPoint - yItemDistance * (i + 1), mPaint); } }
这里有了xData和yData我们可以顺手把写死的线的条数
private void initData() { ....... dp3 = (int) getContext().getResources().getDimension(R.dimen.dp3); //箭头的偏移量 dp6 = (int) getContext().getResources().getDimension(R.dimen.dp6); //箭头的偏移量 yItemDistance = (yPoint - (yTopPoint + mTextSize)) / yData.size(); xItemDistance = (mWidth - xPoint - xPoint) / xData.size(); }
private void drawAxes(Canvas canvas) { ...... //画横着的线 int yTextPoint; //y轴字段的坐标 for (int i = 0; i < yData.size(); i++) { yTextPoint = yTopPoint + mTextSize + yItemDistance * i; canvas.drawLine(xPoint, yTextPoint, mWidth - xPoint, yTextPoint, mPaint); //x轴线 } //画竖着的线 int xTextPoint; //x轴字段的坐标 for (int i = 1; i < xData.size(); i++) { xTextPoint = xPoint + xItemDistance * i; canvas.drawLine(xTextPoint, yPoint, xTextPoint, yTopPoint + mTextSize, mPaint); //y轴线这里的线比起始Y轴短,留出位置画title } }
画完背景我们实现第三个需求
3.折线和点,渐变背景色
分析折线和点
3.1.画折线和折点需要的笔
3.2.各个点的的数据
3.3.连接数据的path
分析渐变背景色
3.4.画背景的笔
3.5.画背景的路线
private void initData() { ...... //3.1.画折线和折点需要的笔 //折线画笔 mLinePaint = new Paint(); mLinePaint.setStyle(Paint.Style.STROKE); mLinePaint.setAntiAlias(true);//去锯齿 mLinePaint.setColor(mLinePaintColor);//颜色 mLinePaint.setTextSize(mTextSize); //画折点的笔 mCirclePaint = new Paint(); mCirclePaint.setStyle(Paint.Style.FILL); mCirclePaint.setAntiAlias(true);//去锯齿 mCirclePaint.setColor(mLinePaintColor);//颜色 // 3.4.画背景的笔 mShawerPaint = new Paint(); mShawerPaint.setAntiAlias(true); mShawerPaint.setStrokeWidth(2f); //3.3.连接数据的path mPath = new Path(); //3.5.连接数据的path mShowerPath = new Path(); //背景的渐变色 shadeColors = new int[]{ Color.argb(80, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)), Color.argb(20, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)), Color.argb(0, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor))}; }
private void initData() { //3.2.各个点的数据 // FIXME: 2017/4/17 itemData.add(15); itemData.add(18); itemData.add(11); itemData.add(25); itemData.add(35); itemData.add(45); itemData.add(0); // FIXME: 2017/4/17 xData.add("4-11"); ...... }
准备完毕可以开始画线了
分析折点的坐标
x点的坐标就是x起点的坐标+x线之间的间距
x = xPoint + xItemDistance * i;
y轴的坐标则需要按比例计算
折点的值/y刻度的范围 = y顶点到y坐标的距离/y刻度件的长度
y坐标 = (yPoint - yItemDistance-((integer * 1.0-yMin) / itemLength) * yLength));
y刻度的范围我们需要yMax,yMin;
画线的成员变量也就基本确定了
private ArrayListitemData = new ArrayList<>(); private Paint mShawerPaint; private Paint mCirclePaint; private Paint mLinePaint; private Path mPath; private int mLinePaintColor = Color.RED; // FIXME: 2017/4/17 private Path mShowerPath; private int[] shadeColors; private int yMax; private int yMin;
private void initData() { ...... shadeColors = new int[]{ Color.argb(80, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)), Color.argb(20, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor)), Color.argb(0, Color.red(mPaintColor), Color.green(mPaintColor), Color.blue(mPaintColor))}; if (yData.size() > 0) { yMin = yData.get(0); yMax = yData.get(0); } for (int i = 0; i < yData.size(); i++) { if (yData.get(i) > yMax) { yMax = yData.get(i); } else if (yData.get(i) < yMin) { yMin = yData.get(i); } } }
做完准备工作就开画吧
private void drawLine(Canvas canvas) { for (int i = 0; i < itemData.size(); i++) { int integer = itemData.get(i); //折点x坐标 int itemX = xPoint + xItemDistance * i; //折点y坐标 int itemLength = yMax - yMin; int yLength = yPoint - (yTopPoint + mTextSize) - yItemDistance; int itemY = 0; if (itemLength != 0) { itemY = (int) (yPoint - yItemDistance - ((integer * 1.0 - yMin) / itemLength) * yLength); } //画折点 canvas.drawCircle((xPoint + xItemDistance * i), itemY, 3, mCirclePaint);//画小圆点 // 连接折线的路径,阴影的路径 if (i == 0) { mPath.moveTo(itemX, itemY); mShowerPath.moveTo(itemX, itemY); } else { mPath.lineTo(itemX, itemY); mShowerPath.lineTo(itemX, itemY); if (i == itemData.size() - 1) { mShowerPath.lineTo(itemX, yPoint - yItemDistance); mShowerPath.lineTo(xPoint, yPoint - yItemDistance); mShowerPath.close(); } } } }
写到这里我们基本的ui图已经都画出来了,但是我们前面的代码就标注了,还有些写死的数据需要改,而且我们的是动态刻度图,自然y轴的刻度也不会让亲们自己去写
我们这里先改简单的attr数据,这里自定义的attr数据有textSize,mPaintColor,mLinePaintColor,如果有其他需求,可以自行添加。
在values下创建attrs文件夹,自定义名字和属性
xml version="1.0" encoding="utf-8"?>name="MyChartView"> name="paintColor" format="color"/> name="textSize" format="dimension"/> name="chartLineColor" format="color"/>
定义完后,我们在view中读取
public MyChartView(Context context) { this(context, null); } public MyChartView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyChartView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); float textSize = getContext().getResources().getDimension(R.dimen.textSize); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyChartView); mPaintColor = typedArray.getColor(R.styleable.MyChartView_paintColor,Color.RED); mLineColor = typedArray.getColor(R.styleable.MyChartView_chartLineColor, Color.RED); mTextSize = (int) typedArray.getDimension(R.styleable.MyChartView_textSize, textSize); typedArray.recycle(); }
我们再来分析一下y轴的刻度;
y轴的刻度如果偏差加大,那刻度的差值也要随之改变,而且可能出现负数,显然y轴刻度不能写死,让用户自己设置也会让控件使用变得麻烦so我们这里在设置itemData的时候也动态修改yData的值。
通过分析这里我把最低刻度和最高刻度都设为10的整数倍,中间值则有折点值来决定
public void setItemData(@NonNull ArrayListitemData) { this.itemData = itemData; if (itemData.size() == 0) { return; } itemMin = itemData.get(0); //折点的最小值 itemMax = itemData.get(0); //折点的最大值 for (int i = 0; i < itemData.size(); i++) { if (itemData.get(i) > itemMax) { itemMax = itemData.get(i); } else if (itemData.get(i) < itemMin) { itemMin = itemData.get(i); } } //设置y轴刻度的最小值为比itemMin小的 最大的10的整数倍 int yMin = itemMin / 10 * 10; if (yMin < 0) { yMin = yMin - 10; } //设置y轴刻度的最大值为比itemMax大的 最小的10的整数倍 int yMax = (itemMax / 10) * 10 + 10; if (itemMax % 10 == 0) { yMax = itemMax / 10 * 10; } yData.clear(); //最大值,最小值都是10的倍数,那么我们的中间刻度也好取了, // 为了刻度美观,我们这里如果yMax-yMin≤50,刻度件的差值就≤10,如果>50,就取10的整数倍 for (int i = 0; i <= 5; i++) { if ((yMax - yMin) / 5 <= 10) { yData.add(yMin + (yMax - yMin) / 5 * i); } else { yData.add(yMin + ((yMax - yMin) / 50 + 1) * 10 * i); } } //动态设置x轴的data Date date = new Date(); long time = date.getTime(); SimpleDateFormat dateFormat = new SimpleDateFormat("M-d"); xData.clear(); for (long i = itemData.size(); i > 0; i--) { long xDataTime = time - 24 * 60 * 60 * 1000 * i; xData.add(dateFormat.format(xDataTime)); } }
写到这里自定义的ChartView就实现的差不多了,我们可以删掉上面写死的数据,如果需要自己设置xData和yData添加如下代码
//注意在代码中调用这两个方法的话都要在调用setItemData()后面调用,否则无效。
public void setyData(ArrayListxData) { this.yData = yData; } public void setxData(ArrayList xData) { this.xData = xData; if (xData.size() != itemData.size()) { try { throw new Exception("XData Count Unmatched Exception"); } catch (Exception e) { e.printStackTrace(); } } }
自定义的代码就写到这里了。
---------------------------------华丽的分割线-------------------------------------
这里说两句题外话
一开始拿到这种需求图我是懵逼的,对于阴影背景的效果实现,我原本想的画渐变的shape当背景色,然后想circleImageView那样擦掉折线上面的颜色使其透明
刚好写需求的当天看郭神的微信公众号刚好出了一篇自定义的View之颜色渐变折线图。
这简直是为我量身打造的有木有 ,老夫敲代码数十载,拿起键盘就是一顿ctrl+c,ctrl+v,瞬间阴影的效果就有了。
郭神千秋万代,一桶浆糊。
郭神链接
完事后看了关于shader的博客,感觉还是挺强大的,有兴趣的小伙伴可以去看看
Shader链接