Android自定义View之直方图和扇形图——ChartView

xiaoxiao2021-02-28  124

先上图

实现

      因为本次需求中只有柱状图和扇形图的需求,所以本次示例将两种类型的图放在了一个view里,先看看自定义属性

<declare-styleable name="ChartView"> <attr name="textColor" format="color" /> <attr name="textSize" format="dimension" /> <attr name="chartType" format="enum"> <enum name="HISTOGRAM" value="101"/> <enum name="FAN" value="102"/> </attr> <attr name="proportion" format="integer"/> <attr name="axisColor" format="color"/> <attr name="sectionColor" format="color"/> <attr name="sectionAmount" format="integer"/> </declare-styleable>

      其中chartType表示图表类型,也就是上面提到的柱状或扇形图,proportion表示在柱状图里,每个柱子与柱子之间的间隔的宽度比,axisColor为坐标轴的颜色,sectionAmount表示坐标轴分成几个区间并且以横线将坐标轴分成对应个区域。

onMeasure

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); //默认以w/h : 16/12 的形式展现 int screenWidth = getResources().getDisplayMetrics().widthPixels; if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(screenWidth, (int) (screenWidth*12f/16f)); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(screenWidth, heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, (int) (screenWidth*12f/16f)); } }

      在onMeasure中对view的高度进行了一定处理,可能情况考虑得不周全,但在布局中将view的高度设为wrap_content就行了,默认高宽比是4:3。

绘制柱状图

      先贴上图标中需要用到的数据模型,下文中用到的mEntities就是该模型的一个列表。

public static class Entity{ private String text; private int amount; private int color; } private void onDrawHistogram(Canvas canvas){ int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int paddingRight = getPaddingRight(); int paddingBottom = getPaddingBottom(); int contentWidth = getWidth() - paddingLeft - paddingRight; int contentHeight = getHeight() - paddingTop - paddingBottom; //柱子的个数 int column = mEntities.size(); //数据中amount最大的值,用该值来决定所有柱子的高度 int maxAmount = 0; for(Entity e : mEntities){ maxAmount = maxAmount < e.getAmount() ? e.getAmount() : maxAmount; } //n表示上面的maxAmount数值的高度为contentHeight的n倍(默认就是3/4) float n = (mSectionAmount-1)*1f/mSectionAmount; //每一个amount单元的高度 float perUnitHeight = contentHeight * n /maxAmount; //计算每列的宽度 int distance = contentWidth / ((mProportion+1)*column+1); //开始绘制 //绘制四条区间线 mChartPaint.setColor(mSectionColor); float unitSectionHeight = (contentHeight) / mSectionAmount; for(int i=0;i<mSectionAmount;i++){ canvas.drawLine(paddingLeft,paddingTop+unitSectionHeight*i,contentWidth,paddingTop+unitSectionHeight*i+1,mChartPaint); } //绘制柱状图 int startX = paddingLeft; for (int i = 0;i < column ;i++){ Entity e = mEntities.get(i); mChartPaint.setColor(e.getColor()); float h = contentHeight - perUnitHeight*e.getAmount(); canvas.drawRect(startX,contentHeight - mAnimValue * (contentHeight-h),startX+mProportion*distance,contentHeight,mChartPaint); //需要注意文字所在的高度计算 canvas.drawText(e.getText(),0,e.getText().length(),startX+mProportion/2f*distance,h-2*mTextSize,mTextPaint); String _amount = String.valueOf(e.getAmount()); canvas.drawText(_amount,0,_amount.length(),startX+mProportion/2f*distance,h-mTextSize,mTextPaint); startX += (mProportion+1)*distance; } //最后画绘制坐标轴,最后画的好处是不会让前面画的柱子挡住x轴 mChartPaint.setColor(mAxisColor); //y轴 canvas.drawLine(paddingLeft,paddingTop,paddingLeft+1,contentHeight,mChartPaint); //x轴 canvas.drawLine(paddingLeft,contentHeight-1,contentWidth,contentHeight,mChartPaint); }

      在代码中有一个mAnimValue变量,它表示给view添加动画的话,当前动画进度的值,后面会介绍动画的实现。

绘制扇形图

private void onoDrawFan(Canvas canvas){ int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int paddingRight = getPaddingRight(); int paddingBottom = getPaddingBottom(); int contentWidth = getWidth() - paddingLeft - paddingRight; int contentHeight = getHeight() - paddingTop - paddingBottom; int length = mEntities.size(); //计算扇形大小, //圆心坐标,圆心坐标当然就定义到view中心位置去了,此处用的是view的高宽,不是上面计算出的content的大小,所以设置padding的时候可能中心位置和预期不太一样(反正在view中心,爱咋咋地) Point pCenter = new Point(getWidth()/2,getHeight()/2); //半径 // int min = contentWidth > contentHeight ? contentHeight :contentWidth; //看到了吧,这里的mProportion又表示扇形所在圆与view内容区域高宽的比例了。 float raduis = (min/2f)*(mProportion-1)/mProportion; //计算总值,用以计算每个entity所占比例 int totalAmount = 0; for(int i = 0; i<length;i++){ totalAmount += mEntities.get(i).getAmount(); } //需求说从正上方顺时针开始画,那就是这个咯(270) double startAngle = -90; //圆所在的区域 RectF rectF = new RectF(pCenter.x-raduis,pCenter.y-raduis,pCenter.x+raduis,pCenter.y+raduis); mChartPaint.setStrokeWidth(2); mChartPaint.setTextSize(mTextSize); for(int i = 0; i<length;i++){ Entity e = mEntities.get(i); //画扇形 //计算扇形的弧度(角度弧度傻傻分不清) double sweepAngle = e.getAmount() *1f / totalAmount * 360 * mAnimValue; mChartPaint.setColor(e.getColor()); canvas.drawArc(rectF,(float)startAngle,(float)sweepAngle,true,mChartPaint); //画文字和折线 /* 折线从每个项的弧形中间出发,向外一定距离后横向一定距离再绘制文字 折线需要三个点 */ //vCos和vSin是下面会重复利用到的值,减少计算量 double vCos = Math.cos((startAngle+sweepAngle/2) * 2 * Math.PI / 360); double vSin = Math.sin((startAngle+sweepAngle/2) * 2 * Math.PI / 360); //起点 int startX = (int) (vCos * raduis)+pCenter.x; int startY = (int) (vSin * raduis)+pCenter.y; // //中点,计算中点就是把半径增到一定距离。默认加的30px,估计不同设备显示不一致,who cares int middleX = (int) (vCos * (raduis+30))+pCenter.x; int middleY = (int) (vSin * (raduis+30))+pCenter.y; //终点,需要根据角度来判断加还是减 int endY = middleY; double angle = (startAngle+sweepAngle/2)%360; int endX = middleX; boolean left = false; if(angle >= 270 || angle <= 90){ endX += 50; left = false; }else{ endX -= 50; left = true; } canvas.drawLine(startX,startY,middleX,middleY,mChartPaint); canvas.drawLine(middleX,middleY,endX,endY,mChartPaint); //以终点为基础绘制文字,文字是align是设置为center的,所以计算出文子所占宽度,大体向左或向右移动一些距离就是那个样子了 float w = mTextPaint.measureText(e.getText()); w = left ? w*-1.2f : w/1.8f;//数值是随便调的 canvas.drawText(e.getText(),endX+w,endY+mTextSize/2,mChartPaint); //最后当然要把开始角度给加起来 startAngle += sweepAngle; }

动画

      动画用起来就简单了,配合ValueAnimator,改变上面的mAnimValue,更新下就可以啦。至于什么时候使用,It’s up to yourself!!! 因为不晓得gif怎么弄,就算啦。

public void startAnimation(){ mValueAnimator = ValueAnimator.ofFloat(0f,1f); mValueAnimator.setDuration(DURATION); mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mAnimValue = (float) animation.getAnimatedValue(); postInvalidate(); } }); mValueAnimator.start(); }

最后

我觉得还是有必要贴下onDraw的代码

protected void onDraw(Canvas canvas) { super.onDraw(canvas); if(mEntities == null || mEntities.size() == 0){ return; } if(mChartType == CHART_TYPE_HISTOGRAM){ onDrawHistogram(canvas); }else if(mChartType == CHART_TYPE_FAN){ onoDrawFan(canvas); } }

      布局中使用

<com.huicheng.jingkaiqu.view.widgets.ChartView xmlns:app="http://schemas.android.com/apk/res-auto" android:background="@color/white" android:id="@+id/chartViewFan" android:layout_width="match_parent" android:layout_height="wrap_content" app:textSize="15sp" app:textColor="@color/textColor" app:chartType="FAN" app:proportion="4" android:padding="12dp"> </com.huicheng.jingkaiqu.view.widgets.ChartView> <com.huicheng.jingkaiqu.view.widgets.ChartView xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/chartView" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/white" android:padding="12dp" app:axisColor="@color/textColor" app:chartType="HISTOGRAM" app:proportion="4" app:sectionAmount="4" app:sectionColor="@color/tab_tv_normal" app:textColor="@color/textColor" app:textSize="15sp"> </com.huicheng.jingkaiqu.view.widgets.ChartView>

Github源码地址

转载请注明原文地址: https://www.6miu.com/read-45702.html

最新回复(0)