Android-自定义贝塞尔曲线图表控件
2017-10-26 13:49
281 查看
写在前面
由于项目需要,下图的图表控件,搜索了各种开源库,没有合适的,只能自定义了。这是我第一次做的自定义控件。写的很渣,请多指教。拆分
该控件可以拆分几个部分进行绘制1. 绘制5条水平分割线
2. 绘制底部横坐标
3. 绘制贝塞尔曲线
4. 绘制圆角矩形标注
5. 绘制垂直线和底部三角形
绘制5条水平分割线
/** *画5条分割线 */ private void drawHorizonLine(Canvas canvas){ mLineTextPaint.setColor(0x66cccccc); mLineTextPaint.setTextSize(DensityUtil.dip2px(mContext,12f)); float intervalY = (getMeasuredHeight()-mMarginTopBottom*2)/4;//每条线段的间隔 for (int i=0; i<5; i++){ canvas.drawLine( mMarginLeftRight, getMeasuredHeight()-mMarginTopBottom-intervalY*i, getMeasuredWidth()-mMarginLeftRight, getMeasuredHeight()-mMarginTopBottom-intervalY*i, mLineTextPaint); } }
绘制底部横坐标
/** *画横坐标 */ private void drawXLabel(Canvas canvas){ float xNameintervalX = (mWidth - 2f * mMarginLeftRight) / (mXNameListShow.size()-1);//横坐标的间隔 mLineTextPaint.setTextSize(DensityUtil.dip2px(mContext,12f)); for (int i=0; i<mXNameListShow.size(); i++){ canvas.drawText(mXNameListShow.get(i), mMarginLeftRight + xNameintervalX*(i)-DensityUtil.dip2px(mContext,12f), getMeasuredHeight() - DensityUtil.dip2px(mContext,10f), mLineTextPaint); canvas.save(); } }
mXNameListShow是横坐标的集合,默认10个以下的数据,通过外部获取
/** * 设置底部时间数据*/ public void setxNameDataList(List<String> xNameDataList){ this.mXNameList = xNameDataList; setxNameListShow(); postInvalidateDelayed(50); }
因为从外部传来的数据有可能会很多记录几百几千个,因此抽样出其中10个以下的数据
/** * 设置显示的时间 * 只显示10个时间点*/ private void setxNameListShow(){ int interval = mXNameList.size()/10+1;//只显示10个横坐标,的间隔 for (int i=0; i<mXNameList.size(); i = i+interval){ mXNameListShow.add(mXNameList.get(i)); } }
01:00, 02:00, 03:00, 04:00, 05:00, 06:00, 07:00, 08:00, 09:00, 10:00
绘制贝塞尔曲线
绘制曲线之前,需要对贝塞尔曲线有最基本的了解。我了解的比较浅显,根据起点,终点和控制点就能绘制出一条(一段)曲线,然后将每一段拼接成一条长长的贝塞尔曲线,通常情况下,只需要3-5点的贝塞尔线拼接成的贝塞尔曲线就很好看了。由于坐标数量很多,所以需要从中抽样出几个点,绘制几段贝赛尔曲线拼接起来。我认为这难点在于求没一段曲线的控制点。绘制二价贝塞尔曲线需要一个控制点,绘制三价曲线需要两个控制点。
怎么求控制点可以参考这里
贝塞尔曲线控制点确定的方法
然后把上面公式转换成代码就成了这样
这个方法用来获取每一段贝塞尔曲线所需要的点数据,包括起点,终点,控制点1,控制点2
pointList为安卓坐标系中的点集合,原始数据是
(1,1)
(2,10)
(3,6)
(4,8)
.
.
.
.
/**获取每一段曲线所需要的点集*/ private List<BezierLineData> getLineData(List<PointF> pointList){ float t = 0.5f;//比例 List<BezierLineData> lineDataList = new ArrayList<>(); PointF startP; PointF endP; PointF cp1; PointF cp2; BezierLineData lineData; for (int i = 0; i<pointList.size() - 1;i ++){ startP = pointList.get(i); endP = pointList.get(i+1); cp1 = new PointF(); cp1.x = startP.x + (endP.x-startP.x) * t; cp1.y = startP.y; cp2 = new PointF(); cp2.x = startP.x + (endP.x-startP.x) * (1 - t); cp2.y = endP.y; lineData = new BezierLineData(startP,endP,cp1,cp2); lineDataList.add(lineData); } return lineDataList; }
BezierLineData定义如下
/** * Created by allever on 17-9-28. * * 每一段曲线用到的数据点集 */ public class BezierLineData< 4000 /span> { private PointF startP; private PointF endP; private PointF cp1; private PointF cp2; public BezierLineData(PointF startP,PointF endP,PointF cp1,PointF cp2){ this.startP = startP; this.endP = endP; this.cp1 = cp1; this.cp2 = cp2; } public PointF getStartP() { return startP; } public void setStartP(PointF startP) { this.startP = startP; } public PointF getEndP() { return endP; } public void setEndP(PointF endP) { this.endP = endP; } public PointF getCp1() { return cp1; } public void setCp1(PointF cp1) { this.cp1 = cp1; } public PointF getCp2() { return cp2; } public void setCp2(PointF cp2) { this.cp2 = cp2; } }
绘制贝塞尔曲线的方法
private void drawBezier2(Canvas canvas){ //绘制前先获取,保存一些数据,原始点(y值),每条曲线每一段的数据点集,绘制标注时用到. initData(); mBezierPaint.setStyle(Paint.Style.STROKE); mBezierPaint.setStrokeWidth(DensityUtil.dip2px(mContext,3f));//设置线宽 mBezierPaint.setAntiAlias(true);//去除锯齿 mBezierPaint.setStrokeJoin(Paint.Join.ROUND); mBezierPaint.setStrokeCap(Paint.Cap.ROUND); for (int i=0;i<mLineDataSetList.size();i++){ Path bezierPath = new Path();//曲线路径 bezierPath.moveTo(mBezierLineDataList.get(i).get(0).getStartP().x,mBezierLineDataList.get(i).get(0).getStartP().y); for (int j=0; j<mBezierLineDataList.get(i).size();j++){ bezierPath.cubicTo( mBezierLineDataList.get(i).get(j).getCp1().x, mBezierLineDataList.get(i).get(j).getCp1().y, mBezierLineDataList.get(i).get(j).getCp2().x, mBezierLineDataList.get(i).get(j).getCp2().y, mBezierLineDataList.get(i).get(j).getEndP().x, mBezierLineDataList.get(i).get(j).getEndP().y); } //设置颜色和渐变 int lineColor = mLineDataSetList.get(i).getColor(); mBezierPaint.setColor(lineColor); LinearGradient mLinearGradient; int[] colorArr; if (mLineDataSetList.get(i).getGradientColors() != null){ colorArr = mLineDataSetList.get(i).getGradientColors(); }else { colorArr = new int[]{lineColor,lineColor,lineColor,lineColor,lineColor}; } mLinearGradient = new LinearGradient( 0, mMarginTopBottom, 0, getMeasuredHeight(), colorArr, null, Shader.TileMode.CLAMP ); mBezierPaint.setShader(mLinearGradient); canvas.drawPath(bezierPath,mBezierPaint); canvas.save(); } }
绘制前先获取,保存一些数据,原始点(y值),每条曲线每一段的数据点集,绘制标注时用到.
initData();方法如下
private void initData(){ List<PointF> aOriginPointList; //List<PointF> aAndroidPointList; //List<BezierLineData> aLineDataList; //List<PointF> aSelectedOriginPointList; mBezierLineDataList.clear(); mOriginPointList.clear(); //mAndroidPointList.clear(); for (LineDataSet lineDataSet: mLineDataSetList){ //每一次遍历就是一条曲线数据 aOriginPointList = lineDataSet.getOldPointFsList(); if (aOriginPointList.size() == 0) continue; mOriginPointList.add(aOriginPointList); //lineDataSet.getOldPointFsList()获取原始坐标 //getSelectedPoint();获取筛选后的数据 //changePoint();将原始点转化为Android的坐标点 //getLineData();获取贝塞尔曲线的点集 mBezierLineDataList.add( getLineData( changePoint( getSelectedPoint( lineDataSet.getOldPointFsList())))); } }
getSelectedPoint(List pointFList);
/** * 从全部数据中选中其中指定个数据*/ private List<PointF> getSelectedPoint(List<PointF> pointFList){ PointF pointF; PointF selectedPoint; float ySum = 0; float averageY; int interval = pointFList.size()/mBezierPointCount+1; List<PointF> selectedPointList = new ArrayList<>(); if (pointFList.size()==0 ) return selectedPointList; int j=0; for (int i=0; i<pointFList.size();i++){ pointF = pointFList.get(i); ySum += pointF.y; if (i%interval==0){ j++; averageY = ySum/interval; //selectedPoint = new PointF(j, averageY);//求平均 selectedPoint = new PointF(j, pointF.y);//不求平均 selectedPointList.add(selectedPoint); ySum = 0; } } Log.d(TAG, "getSelectedPoint: selected count = " + selectedPointList.size()); //暂时办法-解决不够n个点 if (selectedPointList.size() < mBezierPointCount){ int curPosition; for (curPosition= selectedPointList.size();curPosition<mBezierPointCount; curPosition++){ selectedPointList.add(new PointF(curPosition+1,selectedPointList.get(selectedPointList.size()-1).y)); } } Log.d(TAG, "getSelectedPoint: after selected count = " + selectedPointList.size()); return selectedPointList; }
changePoint()
/** * 把一般坐标转为 Android中的视图坐标**/ private List<PointF> changePoint(List<PointF> oldPointFs){ List<PointF> pointFs = new ArrayList<>(); float maxValueY = 0; float yValue; for (int i = 0; i < oldPointFs.size(); i++){ yValue = oldPointFs.get(i).y; if (maxValueY < yValue) maxValueY = yValue+ (yValue*0.1f);// } Log.d(TAG, "changePoint: maxValueY = " + maxValueY); //间隔,减去某个值是为了空出多余空间,为了画线以外,还要写坐标轴的值,除以坐标轴最大值 //相当于缩小图像 int blockCount = oldPointFs.size() - 1; float intervalX = (getMeasuredWidth() - mMarginLeftRight * 2f)/blockCount; float intervalY = (getMeasuredHeight() - mMarginTopBottom * 2f)/maxValueY-0f; int height = getMeasuredHeight(); PointF p; float x; float y; for (int i = 0; i< oldPointFs.size(); i++){ PointF pointF = oldPointFs.get(i); //最后的正负值是左移右移 x = (pointF.x-1) * intervalX + mMarginLeftRight; y = height - mMarginTopBottom - intervalY*pointF.y - DensityUtil.dip2px(mContext,5f); p = new PointF(x, y); pointFs.add(p); } return pointFs; }
绘制曲线的核心代码
绘制曲线的方法是,初始化一个路径对象,设置起点,绘制路径
path.moveTo(startPx, startPy);
path.cubicTo(cp1x, cp1y, cp2x, cp2y, endPx, endPy);
for (int i=0;i<mLineDataSetList.size();i++){//绘制n条曲线 Path bezierPath = new Path();//曲线路径 bezierPath.moveTo(mBezierLineDataList.get(i).get(0).getStartP().x,mBezierLineDataList.get(i).get(0).getStartP().y);//移动到起点 //循环绘制路径 for (int j=0; j<mBezierLineDataList.get(i).size();j++){// bezierPath.cubicTo( mBezierLineDataList.get(i).get(j).getCp1().x, mBezierLineDataList.get(i).get(j).getCp1().y, mBezierLineDataList.get(i).get(j).getCp2().x, mBezierLineDataList.get(i).get(j).getCp2().y, mBezierLineDataList.get(i).get(j).getEndP().x, mBezierLineDataList.get(i).get(j).getEndP().y); } //设置颜色和渐变 int lineColor = mLineDataSetList.get(i).getColor(); mBezierPaint.setColor(lineColor); LinearGradient mLinearGradient; int[] colorArr; if (mLineDataSetList.get(i).getGradientColors() != null){ colorArr = mLineDataSetList.get(i).getGradientColors(); }else { colorArr = new int[]{lineColor,lineColor,lineColor,lineColor,lineColor}; } mLinearGradient = new LinearGradient( 0, mMarginTopBottom, 0, getMeasuredHeight(), colorArr, null, Shader.TileMode.CLAMP ); mBezierPaint.setShader(mLinearGradient); canvas.drawPath(bezierPath,mBezierPaint); canvas.save(); }
曲线参数对象
/** * Created by allever on 17-8-10. * 每一条线对应一个对象 */ public class LineDataSet { private int color;//颜色, private int[] gradientColors;//渐变色数组 private List<PointF> oldPointFsList;//原始点 private SportAnalysisType sportAnalysisType;//参数类型,项目中用到,实际上该字段无用 public int getColor() { return color; } public void setColor(int color) { this.color = color; } public int[] getGradientColors() { return gradientColors; } public void setGradientColors(int[] gradientColors) { this.gradientColors = gradientColors; } public List<PointF> getOldPointFsList() { return oldPointFsList; } public void setOldPointFsList(List<PointF> oldPointFsList) { this.oldPointFsList = oldPointFsList; } public SportAnalysisType getSportAnalysisType() { return sportAnalysisType; } public void setSportAnalysisType(SportAnalysisType sportAnalysisType) { this.sportAnalysisType = sportAnalysisType; } }
绘制圆角矩形标注
难点在于,求曲线上的y坐标(Android坐标系)可以根据公式来求
/** * B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1] * * @param t 曲线长度比例 * @param p0 起始点 * @param p1 控制点1 * @param p2 控制点2 * @param p3 终止点 * @return t对应的点 */ public static PointF calculateBezierPointForCubic(float t, PointF p0, PointF p1, PointF p2, PointF p3) { PointF point = new PointF(); float temp = 1 - t; point.x = p0.x * temp * temp * temp + 3 * p1.x * t * temp * temp + 3 * p2.x * t * t * temp + p3.x * t * t * t; point.y = p0.y * temp * temp * temp + 3 * p1.y * t * temp * temp + 3 * p2.y * t * t * temp + p3.y * t * t * t; return point; }
/** * 绘制标注*/ private void drawMark2(Canvas canvas){ if (mDownX == -1) return; if (mDownX < mMarginLeftRight || mDownX > mWidth-mMarginLeftRight ) return; List<BezierLineData> lineDataList; BezierLineData lineData; float t;//点在曲线上的长度比例 float intevalBezierX = (mWidth-2*mMarginLeftRight)/((float)(mBezierPointCount-1)); PointF linePoint;//曲线上的坐标点 for(int i=0; i<mBezierLineDataList.size();i++){//曲线数量 //设置该条曲线的颜色和渐变,画笔 initLineStyle(i); //获取该条曲线每段曲线的数据集合 lineDataList = mBezierLineDataList.get(i); //判断触控点在哪一段曲线上 int bezierLinePosition = -1;//曲线段数索引 for (int n = 0;n<lineDataList.size();n++){ if ((mDownX > intevalBezierX*n + mMarginLeftRight) && ((mDownX < intevalBezierX*(n+1)+mMarginLeftRight))){ bezierLinePosition = n; break; } } if (bezierLinePosition == -1 ) return; //根据段数获取该段曲线的数据点集合(起点,终点,控制点) lineData = lineDataList.get(bezierLinePosition); //求触控点在该段曲线上的长度比例 t = (mDownX-lineData.getStartP().x)/intevalBezierX; linePoint = BezierUtil.calculateBezierPointForCubic(t,lineData.getStartP(),lineData.getCp1(),lineData.getCp2(),lineData.getEndP()); //求Marker上显示的数值 float value =0; //根据触控点所在区间求y值(真实数据) int position = -1;//触摸点所在区间 float intervalDataX = (mWidth-2*mMarginLeftRight)/((float)(mOriginPointList.get(i).size())); for (int m=0;m<mOriginPointList.get(i).size();m++){ if ((mDownX > (intervalDataX*m+mMarginLeftRight) ) && (mDownX < (intervalDataX*(m+1)+mMarginLeftRight))){ position = m; break; } } if (position != -1 )value = mOriginPointList.get(i).get(position).y; value = (float)(Math.round(value*1000))/1000; String drawText = value + " " + mLineDataSetList.get(i).getSportAnalysisType().getUnit(); //Marker的宽高 float dataBoxWidth = DensityUtil.dip2px(mContext,drawText.length()*7.7f);//标注边框宽度//根据文字长度动态变化 float dataBoxHeight = DensityUtil.dip2px(mContext,30); if (i%2==0){//根据曲线索引判断在左边还是在右边绘制 //处理边界,超出边界时在另一边绘制 if (mWidth-mMarginLeftRight-mDownX < dataBoxWidth/2){ //左边绘制 drawLeft(canvas,linePoint,drawText,dataBoxWidth,dataBoxHeight); }else { //右边绘制 drawRight(canvas,linePoint,drawText,dataBoxWidth,dataBoxHeight); } }else { if (mDownX - mMarginLeftRight < dataBoxWidth/2){ //右边绘制 drawRight(canvas,linePoint,drawText,dataBoxWidth,dataBoxHeight); }else { //左边绘制 drawLeft(canvas,linePoint,drawText,dataBoxWidth,dataBoxHeight); } } } }
drawLeft()和drawRight()
private void drawRight(Canvas canvas, PointF linePoint, String drawText,float dataBoxWidth,float dataBoxHeight){ canvas.drawRoundRect( new RectF( mDownX + DensityUtil.dip2px(mContext,3f), linePoint.y + DensityUtil.dip2px(mContext,5f), mDownX + dataBoxWidth + DensityUtil.dip2px(mContext,3f), linePoint.y + dataBoxHeight), DensityUtil.dip2px(mContext,8f), DensityUtil.dip2px(mContext,8f), mRectPaint); canvas.drawText(drawText,0, drawText.length(), mDownX +DensityUtil.dip2px(mContext,8f), linePoint.y+DensityUtil.dip2px(mContext,21f), mTextPaint); } private void drawLeft(Canvas canvas, PointF linePoint, String drawText,float dataBoxWidth,float dataBoxHeight){ canvas.drawRoundRect( new RectF( mDownX - dataBoxWidth - DensityUtil.dip2px(mContext,3f), linePoint.y + DensityUtil.dip2px(mContext,5f), mDownX - DensityUtil.dip2px(mContext,3f), linePoint.y + dataBoxHeight), DensityUtil.dip2px(mContext,8f), DensityUtil.dip2px(mContext,8f), mRectPaint); canvas.drawText(drawText,0, drawText.length(), mDownX - dataBoxWidth + DensityUtil.dip2px(mContext,3f), linePoint.y+DensityUtil.dip2px(mContext,21f), mTextPaint); }
绘制垂直线和底部三角形
/** * 绘制标线及底部三角形*/ private void drawMarkLine2(Canvas canvas){ if (mDownX == -1) return; if (mDownX < mMarginLeftRight || mDownX > mWidth-mMarginLeftRight ) return; mVerticalPaint.setColor(Color.WHITE); mVerticalPaint.setStrokeWidth(2f); Path trianglePath = new Path(); canvas.drawLine( mDownX, mMarginTopBottom, mDownX, mHeight - mMarginTopBottom, mVerticalPaint); trianglePath.moveTo(mDownX, mHeight - mMarginTopBottom - 20f); trianglePath.lineTo(mDownX - 20f, mHeight - mMarginTopBottom); trianglePath.lineTo(mDownX + 20f, mHeight - mMarginTopBottom); trianglePath.close(); canvas.drawPath(trianglePath,mVerticalPaint); mDownX = -1; }
处理滑动显示
大概思路就是监听到移动事件时,记录当前按下x坐标值,然后重绘制@Override public boolean onTouchEvent(MotionEvent event) { float downX = event.getX(); switch (event.getAction()){ case MotionEvent.ACTION_DOWN: mDownX = downX; //postInvalidateDelayed(50); Log.d(TAG, "onTouchEvent: ACTION_DOWN mDownX = " + mDownX); invalidate(); break; case MotionEvent.ACTION_MOVE: Log.d(TAG, "onTouchEvent: ACTION_MOVE mDownX = " + mDownX); mDownX = event.getX(); invalidate(); //postInvalidateDelayed(50); break; case MotionEvent.ACTION_UP: break; } //postInvalidateDelayed(50); return true; }
相关文章推荐
- 初识Android自定义控件之圆形百分比图表的制作
- Android自定义View(RollWeekView-炫酷的星期日期选择控件)
- Android好看的自定义滚动式日期选择控件和数字选择器
- Android自定义控件之基本原理
- Android自定义滑动确认控件SlideView
- android 自定义轮播广告控件ViewPager——自定义控件学习(四)
- Android自定义View-- 贝塞尔曲线
- Android自定义控件之柱型图表控件
- Android自定义组合控件——简单明了
- Android:自定义Activity基类,复用布局/控件,减少设置控件的重复代码,onPostOnCreate
- Android自定义View 简单实现多图片选择控件
- Android自定义控件之简介
- Android自定义时间控件选择开始时间到结束时间
- Android 自定义dialog,实现右上角显示一个控件按钮
- Android自定义“图片+文字”控件四种实现方法
- Android开源的精美日历控件,热插拔设计的万能自定义UI
- Android 自定义价格日历控件
- Android 从0开始自定义控件之 View 的滑动冲突详解(四)
- Android自定义SwitchButton左右滑动开关按钮控件
- Android操作系统图表控件TeeChart Java