目前github上有多个关于图表的框架,好比MPAndroidChart很好,可是很大,不必由于一个小的图标让工程项目扩大不少,另外有些轻量级的框架,可是我的感受都很难知足本身的需求,再者就算很好的框架,那也是别人的,只有本身动手写起来,了解前先后后的坑,本身才能成长,并且在写的过程,咱们能发现更多的细节,好比绘制的时候内存分配的问题,Canvas直接绘制和经过Bitmap绘制等等,因此这篇文章的目的:git
因为屏幕的宽度有限,因此咱们一屏通过计算,最好显示的7个点,因此咱们首先须要对咱们的view宽度进行计算,首先拿到屏幕的宽度,而后再进行/7,获得每一个间隔的宽度,而后乘以咱们x的坐标点的个数,其中的onMeasure的方法:github
int widthParentMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
int widthParentMeasureSize = MeasureSpec.getSize(widthMeasureSpec);
int heightParentMeasureMode = MeasureSpec.getMode(heightMeasureSpec);
int heightParentMeasureSize = MeasureSpec.getSize(heightMeasureSpec);
int resultWidthSize = 0;
int resultHeightSize = 0;
int resultWidthMode = MeasureSpec.EXACTLY;//用来对childView进行计算的
int resultHeightMode = MeasureSpec.EXACTLY;
int paddingWidth = getPaddingLeft() + getPaddingRight();
int paddingHeight = getPaddingTop() + getPaddingBottom();
ViewGroup.LayoutParams thisLp = getLayoutParams();
switch (widthParentMeasureMode) {
//父类不加限制给子类
case MeasureSpec.UNSPECIFIED:
//这个表明在布局写死了宽度
if (thisLp.width > 0) {
resultWidthSize = thisLp.width;
resultWidthMode = MeasureSpec.EXACTLY;
} else {
resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);
resultWidthMode = MeasureSpec.UNSPECIFIED;
}
break;
case MeasureSpec.AT_MOST:
//这个表明在布局写死了宽度
if (thisLp.width > 0) {
resultWidthSize = thisLp.width;
resultWidthMode = MeasureSpec.EXACTLY;
} else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
resultWidthSize = Math.max(0, widthParentMeasureSize - paddingWidth);
resultWidthMode = MeasureSpec.AT_MOST;
} else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);
resultWidthMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.EXACTLY:
//这个表明在布局写死了宽度
if (thisLp.width > 0) {
resultWidthSize = Math.min(widthParentMeasureSize, thisLp.width);
resultWidthMode = MeasureSpec.EXACTLY;
} else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
resultWidthSize = widthParentMeasureSize;
resultWidthMode = MeasureSpec.EXACTLY;
} else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
resultWidthSize = (int) (getYMaxTextWidth() + mXinterval * mXdots.length);
resultWidthMode = MeasureSpec.AT_MOST;
}
break;
}
switch (heightParentMeasureMode) {
//父view不加限制
case MeasureSpec.UNSPECIFIED:
//这个表明在布局写死了宽度
if (thisLp.height > 0) {
resultHeightSize = thisLp.height;
resultHeightMode = MeasureSpec.EXACTLY;
} else {
resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());
resultHeightMode = MeasureSpec.UNSPECIFIED;
}
break;
case MeasureSpec.AT_MOST:
if (thisLp.height > 0) {
resultHeightSize = heightParentMeasureSize;
resultHeightMode = MeasureSpec.EXACTLY;
} else if (thisLp.height == ViewGroup.LayoutParams.MATCH_PARENT) {
resultHeightSize = Math.max(0, heightParentMeasureSize - paddingHeight);
resultHeightMode = MeasureSpec.AT_MOST;
} else if (thisLp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());
resultHeightMode = MeasureSpec.UNSPECIFIED;
}
break;
case MeasureSpec.EXACTLY:
//这个表明在布局写死了宽度
if (thisLp.height > 0) {
resultHeightSize = Math.min(heightParentMeasureSize, getMeasuredWidth());
resultHeightMode = MeasureSpec.EXACTLY;
} else if (thisLp.width == ViewGroup.LayoutParams.MATCH_PARENT) {
resultHeightSize = heightParentMeasureSize;
resultHeightMode = MeasureSpec.EXACTLY;
} else if (thisLp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
resultHeightSize = (int) (getYMaxTextHeight() + mYvisibleNum * mYinterval + getXMaxTextHeight());
resultHeightMode = MeasureSpec.AT_MOST;
}
break;
}
setMeasuredDimension(MeasureSpec.makeMeasureSpec(resultWidthSize, resultWidthMode),
MeasureSpec.makeMeasureSpec(resultHeightSize, resultHeightMode));复制代码
设置好了尺寸,咱们就能够绘制界面,这里咱们onDraw的时候,就依次绘制横线和竖线,在绘制横线的时候,将Y坐标的数字一块儿绘制上去,同理绘制竖线的时候,把x坐标的数字绘制上去,折线的画根据数字计算出坐标点,而后建立一个path,首先moveTo(firstX,firstY),而后lineTo下面的点就能够了,最后绘制上path,然而这样的话,咱们在滑动的时候,会发现这个view都会跟着一块儿滚动了,那么咱们怎样才能实现view的部分pinned呢?在这个时候,咱们就须要先建立一个bitmap,将须要滑动的部分绘制到这个bitmap上去,而后bitmap在绘制到这个canvas上的时候,保持固定的位置就好了,好了再说就懵逼了,仍是上代码吧:canvas
float tempTableLeftPadding = getYMaxTextWidth();
if (mBitmap == null || mYNumCanvas == null) {
mBitmap = Bitmap.createBitmap((int) (getMeasuredWidth() - getYMaxTextWidth()), getMeasuredHeight(), Bitmap.Config.ARGB_8888);
mYNumCanvas = new Canvas(mBitmap);
}
mYNumCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mYNumCanvas.translate(mScrollPosX,0);//这段代码就是来实现滑动的操做
//绘制横线
for (int y = 0, size = mYdots.length; y < size; y++) {
String tempText = String.valueOf(mYdots[mYdots.length - 1 - y]);
mYNumCanvas.drawLine(0, (float) (mYinterval * y), (float) (mXdots.length * mXinterval), (float) (mYinterval * y), mXlinePaint);
canvas.drawText(tempText, getYMaxTextWidth() - mYNumPaint.measureText(tempText), getYMaxTextHeight() + (float) (mYinterval * y), mYNumPaint);
}
//绘制竖线
for (int x = 0, size = mXdots.length; x <= size; x++) {
mYNumCanvas.drawLine((float) (mXinterval * x), 0, (float) (mXinterval * x), (float) (mYinterval * mYvisibleNum), mXlinePaint);
if (x >= 1) {
String tempText = mXdots[x - 1];
mYNumCanvas.drawText(tempText, (float) (mXinterval * x) - mYNumPaint.measureText(tempText) / 2, (float) (mYvisibleNum * mYinterval + getYMaxTextHeight()), mYNumPaint);
}
}
if (isAnimationOpen)//是否须要开启动画绘制,这个后面会解释实现方式
mYNumCanvas.drawPath(mLineDrawPath, mLinePaint);
else
mYNumCanvas.drawPath(mLinePath, mLinePaint);
canvas.drawBitmap(mBitmap, tempTableLeftPadding, getYMaxTextHeight() / 2, null);复制代码
上面的mScrollPosX是根据手势监听类GestureDetector来获取的:api
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isAnimationOpen || isDrawOver)
return mGestureDetector.onTouchEvent(event);
return super.onTouchEvent(event);
}复制代码
然而绘制了,咱们感受还缺乏了什么,嗯,没错就是动画效果,这里咱们用到经过的path绘制实现动画的方案,就是先经过PathMeasure获得path的长度,而后根据动画时间,经过ValueAnimator计算它在某个时刻的坐标,而后从新进行绘制path路径:bash
private void startPathAnim(long duration) {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mLineLength);
valueAnimator.setDuration(duration);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
// 获取当前点坐标封装到mCurrentPosition
mPathMeasure.getPosTan(value, mCurrentPosition, null);
mLineDrawPath.lineTo(mCurrentPosition[0], mCurrentPosition[1]);
invalidate();
}
});
valueAnimator.start();
}复制代码
private void drawText(Canvas canvas, float sweepAngle, float startAngle, ArcVo temp) {
float middleAngle;
middleAngle = startAngle + sweepAngle / 2;
float startX;
float startY;
float endX;
float endY;
String drawText = temp.getPercentInCircle() * 100 + "%";
if (middleAngle <= 90) {
//在第四象限
double angle = middleAngle;
angle = Math.toRadians(angle);
startY = endY = (float) (Math.sin(angle) * mRaduis + mRaduis);
endX = (float) (mRaduis + Math.cos(angle) * mRaduis);
startX = endX - UiUtils.getTextWidth(drawText, mTextPaint);
} else if (middleAngle <= 180) {
//在第三象限
double angle = 180 - middleAngle;
angle = Math.toRadians(angle);
startY = endY = (float) (Math.sin(angle) * mRaduis + mRaduis);
startX = (float) (mRaduis - Math.cos(angle) * mRaduis);
endX = startX + UiUtils.getTextWidth(drawText, mTextPaint);
} else if (middleAngle <= 270) {
//在第二象限
double angle = 270 - middleAngle;
angle = Math.toRadians(angle);
startY = endY = (float) (mRaduis - Math.cos(angle) * mRaduis);
startX = (float) (mRaduis - Math.sin(angle) * mRaduis);
endX = startX + UiUtils.getTextWidth(drawText, mTextPaint);
} else {
//在第一象限
double angle = 360 - middleAngle;
angle = Math.toRadians(angle);
startY = endY = (float) (mRaduis - Math.sin(angle) * mRaduis);
endX = (float) (mRaduis + Math.cos(angle) * mRaduis);
startX = endX - UiUtils.getTextWidth(drawText, mTextPaint);
}
mTextPath.reset();
mTextPath.moveTo(startX, startY);
mTextPath.lineTo(endX, endY);
if (middleAngle > 180) {
canvas.drawTextOnPath(drawText, mTextPath, 0, UiUtils.getTextHeight(drawText, mTextPaint), mTextPaint);
} else {
canvas.drawTextOnPath(drawText, mTextPath, 0, -UiUtils.getTextHeight(drawText, mTextPaint), mTextPaint);
}
}
@Override
protected void onDraw(Canvas canvas) {
if (!canDraw()) return;
float sweepAngle;
float startAngle = 0;
for (int i = 0, size = mDisArcList.size(); i < size; i++) {
ArcVo temp = mDisArcList.get(i);
mArcPaint.setColor(temp.getScanColor());
sweepAngle = temp.getPercentInCircle() * 360;
canvas.drawArc(mDrawCircleRect, startAngle, sweepAngle, true, mArcPaint);
drawText(canvas, sweepAngle, startAngle, temp);
startAngle = startAngle + sweepAngle;
}
}复制代码
若是你以为大家的项目正好要用到相似的图标,在项目的gradle文件中,增长compile 'wellijohn.org.simplelinechart:linechart:0.0.2'具体的方法,欢迎移步到github上去看,已经封装成库上传至jcenter,上面有具体的使用方法(图表地址),目前暴露的方法很少,能够留言增长
github地址:github.com/WelliJohn/L…)
若是以为项目对大家的自定义view有必定的启发的话,麻烦帮忙star一下,若是有更好的实现方案,欢迎留言交流!!框架