先说一下坐标轴吧
横轴纵轴是一条Path,长度和高度是按照控件宽度设定的,这样保证在不同手机上比例相同
包括箭头也是path,比较简单
/**
* 画坐标轴
* @param canvas
* @param paint
*/
private void drawAxes(Canvas canvas, Paint paint) {
//坐标轴
Path path = new Path();//三角形
path.moveTo(width/10, width/20);
path.lineTo(width/10, width/2);
path.lineTo(width*9/10, width/2);
canvas.drawPath(path, paint);
//向上箭头
Path pathArrowTop=new Path();
pathArrowTop.moveTo(width/10-width/60,width/20+width/60);
pathArrowTop.lineTo(width/10, width/20);
pathArrowTop.lineTo(width/10+width/60, width/20+width/60);
canvas.drawPath(pathArrowTop, paint);
//向下箭头
Path pathArrowRight=new Path();
pathArrowRight.moveTo(width*9/10-width/60, width/2-width/60);
pathArrowRight.lineTo(width*9/10, width/2);
pathArrowRight.lineTo(width*9/10-width/60, width/2+width/60);
canvas.drawPath(pathArrowRight, paint);
}
横纵坐标的数字是写死的几个值,还没有添加动态设置方法
纵轴数字的间距是把纵轴高度除以纵轴数字数量
起点为原点
注意要测量字体宽高
Rect rect = new Rect();
paintText.getTextBounds(textVertical[i], 0, textVertical[i].length(), rect);
int textWidth = rect.width();
int textHeight = rect.height();
X轴要减去字体宽度,Y轴要加上字体高度的一半,同时添加了一个间距textSpace,以保证字体不与坐标轴重叠,同时又处于当前数值所在纵轴位置的中心(横轴同理)
canvas.drawText(textVertical[i],width/10-textWidth-textSpace,width/2-(i+1)*width/10+textHeight/2,paintText);
private String[] textVertical ={"100","200","300","400"};
private String[] textHorizontal ={"0","1","2","3","4","5","6","7"};
/**
* 坐标轴汉字
* @param canvas
*/
private void drawText(Canvas canvas) {
paintText.setTextSize(width/30);
//横轴
for (int i = 0; i < textHorizontal.length; i++) {
Rect rect = new Rect();
paintText.getTextBounds(textHorizontal[i], 0, textHorizontal[i].length(), rect);
int textWidth = rect.width();
int textHeight = rect.height();
canvas.drawText(textHorizontal[i],width/10+i*width/10-textWidth/2,width/2+textHeight+textSpace,paintText);
}
//纵轴
for (int i = 0; i < textVertical.length; i++) {
Rect rect = new Rect();
paintText.getTextBounds(textVertical[i], 0, textVertical[i].length(), rect);
int textWidth = rect.width();
int textHeight = rect.height();
canvas.drawText(textVertical[i],width/10-textWidth-textSpace,width/2-(i+1)*width/10+textHeight/2,paintText);
}
}
核心部分,画折线数据(随机数,最大设定了400)
也是画一个path
起点为原点 (width/10, width/2)
所经各点的横坐标为 原点横坐标+第几个点横轴每个点间距 width(i+2)/10
纵坐标为 原点纵坐标 -(当前数值/100)*(width/10)
value为ValueAnimator当前值(动画)
for (int i = 0; i < randomNums.length; i++) {
pathData.lineTo(width*(i+2)/10, width/2-value*randomNums[i]*width/1000);
}
/**
* 画数据
* @param canvas
*/
private void drawData(Canvas canvas) {
if(value==0) return;
//根据重力数据画阴影
paintData.setShadowLayer(5,gravityX*shadowWidth,-gravityY*shadowWidth,Color.LTGRAY);
Path pathData=new Path();
pathData.moveTo(width/10, width/2);
for (int i = 0; i < randomNums.length; i++) {
pathData.lineTo(width*(i+2)/10, width/2-value*randomNums[i]*width/1000);
}
canvas.drawPath(pathData, paintData);
}
显示当前数值大大小,其位置是折线的拐点,同数据位置,注意宽高的加减,保证不遮挡拐点,并与拐点中心对其
/**
* 动态数值
* @param canvas
*/
private void drawNum(Canvas canvas) {
paintNum.setTextSize(width/30);
if(value==0) return;
for (int i = 0; i < randomNums.length; i++) {
Rect rect = new Rect();
paintNum.getTextBounds((int)(value*randomNums[i])+"", 0, String.valueOf((int)(value*randomNums[i])).length(), rect);
int textWidth = rect.width();
int textHeight = rect.height();
canvas.drawText((int)(value*randomNums[i])+"",width*(i+2)/10-textWidth/2, width/2-value*randomNums[i]*width/1000-textHeight/2,paintNum);
}
}
点击事件
ACTION_DOWN时判断点击位置与所有拐点的距离,如果距离小于限定范围则认为点击有效
记录点击位置 clickPosition=i ;
将可点击状态置为 isClick=true;
同时启动按下的动画 downAnim();
ACTION_MOVE时判断当前位置与ACTION_DOWN时的位置间距,如果距离大于限定值则取消按下动画并将可点击状态置为 isClick=false;
ACTION_UP时先判断可点击状态是否为true,如果为true则执行抬起水波纹动画
Util.getDistance(downX,downY,width*(i+2)/10,width/2-randomNums[i]*width/1000)
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//获取屏幕上点击的坐标
downX = event.getX();
downY = event.getY();
for (int i = 0; i < randomNums.length; i++) {
if(Util.getDistance(downX,downY,width*(i+2)/10,width/2-randomNums[i]*width/1000)width/15){
if(valueAnimatorDowm.isRunning()){
valueAnimatorDowm.cancel();
}
downValue=2;
isClick=false;
invalidate();
return false;
}
break;
case MotionEvent.ACTION_UP:
if(mOnPointClickListener!=null&&isClick){
mOnPointClickListener.onPointClick(clickPosition,randomNums[clickPosition]);
rippleAmin();
}
return true;
}
return super.onTouchEvent(event);
}
位置也为拐点坐标
设置RadialGradient用于中心渐变色
同时也设定了透明度
半径为 downValue*半径
/**
* 按下水波纹特效
* @param canvas
*/
private void drawDownRipple(Canvas canvas) {
paintDown.setAlpha((int)(255*(1-downValue/2)));
Shader mShader = new RadialGradient(width*(clickPosition+2)/10,width/2-randomNums[clickPosition]*width/1000,width/22,0x00878787,Color.GRAY,Shader.TileMode.CLAMP);
paintDown.setShader(mShader);
canvas.drawCircle(width*(clickPosition+2)/10,width/2-randomNums[clickPosition]*width/1000,downValue*width/22,paintDown);
}
/**
* 抬起水波纹特效
* @param canvas
*/
private void drawRipple(Canvas canvas) {
paintRipple.setAlpha((int)(255*(1-rippleValue)));
Shader mShader = new RadialGradient(width*(clickPosition+2)/10,width/2-randomNums[clickPosition]*width/1000,width/22,0x00878787,Color.DKGRAY,Shader.TileMode.REPEAT);
paintRipple.setShader(mShader);
canvas.drawCircle(width*(clickPosition+2)/10,width/2-randomNums[clickPosition]*width/1000,rippleValue*width/22,paintRipple);
}
动态阴影
不调用setGravity方法有默认静态阴影
//方向数据
private float gravityX=4;
private float gravityY=-4;
private float gravityZ=4;
/**
* 获取手机方向(为避免连续绘制UI,设置数据变化范围大于0.1才刷新UI)
* @param x
* @param y
* @param z
*/
public void setGravity(float x,float y,float z){
if(Math.abs(x-gravityX)>0.1||Math.abs(y-gravityY)>0.1||Math.abs(z-gravityZ)>0.1){
gravityX=x;
gravityY=y;
gravityZ=z;
invalidate();
}
}
paintData.setShadowLayer(5,gravityX*shadowWidth,-gravityY*shadowWidth,Color.LTGRAY);
重力数据获取
在Activity的onCreate中
mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
同时在其他生命周期里适时调用注册传感器监听与取消注册,避免软件后台调用监听,只在页面展示时监听即可
@Override
protected void onResume() {
super.onResume();
mSensorManager.registerListener(this,
mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY),
SensorManager.SENSOR_DELAY_GAME);
}
@Override
protected void onStop() {
// 程序退出时取消注册传感器监听器
mSensorManager.unregisterListener(this);
super.onStop();
}
@Override
protected void onPause() {
// 程序暂停时取消注册传感器监听器
mSensorManager.unregisterListener(this);
super.onPause();
}
监听回调,此调用view的setGravity,让view刷新Ui
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
if(sensorEvent.sensor.getType() == Sensor.TYPE_GRAVITY){
float gravityX=sensorEvent.values[0];
float gravityY=sensorEvent.values[1];
float gravityZ=sensorEvent.values[2];
lineView.setGravity(gravityX,gravityY,gravityZ);
}
}
点击事件
//点击
public interface OnPointClickListener{
void onPointClick(int position, int number);
}
public void setOnPointClickListener(OnPointClickListener listener){
this.mOnPointClickListener=listener;
}
case MotionEvent.ACTION_UP:
if(mOnPointClickListener!=null&&isClick){
mOnPointClickListener.onPointClick(clickPosition,randomNums[clickPosition]);
rippleAmin();
}
return true;
条形统计图原理与折线相同,不再描述,区别在于折线绘制了path路径,条形绘制了rect
细节区别前往GitHub查看Demo
/**
* 画数据
* @param canvas
*/
private void drawData(Canvas canvas) {
//根据重力数据画阴影
paintData.setShadowLayer(10,gravityX*shadowWidth,-gravityY*shadowWidth,Color.DKGRAY);
for (int i = 0; i < randomNums.length; i++) {
paintData.setColor(calculateColor((int)(value*randomNums[i])));
canvas.drawRect(width*(i+2)/10-width/30,width/2-value*randomNums[i]*width/1000,width*(i+2)/10+width/30,width/2,paintData);
}
}
扇形统计图
重点在于每条数据的开始位置角度以及扫过的角度的计算
float startAngle=value*360*getSum(i-1)/sum;
float swipeAngle=value*360*randomNums[i]/sum;
/**
* 获取数据总数
* @return
*/
private int getSum(int position) {
int sum=0;
for (int i = 0; i <= position; i++) {
sum=sum+randomNums[i];
}
return sum;
}
/**
* 画扇形
* @param canvas
*/
private int sum=0;
private RectF rectF;
private void drawSwipe(Canvas canvas) {
if(sum==0||value==0.0f) return;
rectF=new RectF(width/5,width/5,width*4/5,width*4/5);
//根据重力数据画阴影
paint.setShadowLayer(10,gravityX*shadowWidth,-gravityY*shadowWidth,Color.DKGRAY);
for (int i = 0; i < randomNums.length; i++) {
paint.setColor(colors[i]);
float startAngle=value*360*getSum(i-1)/sum;
float swipeAngle=value*360*randomNums[i]/sum;
canvas.drawArc(rectF,startAngle,swipeAngle,true,paint);
}
}
画百分比
位置在所表示扇形位置的中心与圆心连线上(三角函数计算)
float textX= (float) (width/2 + Math.sin(2*Math.PI/360*(90-360*(getSum(i-1)+randomNums[i]/2)/sum))*width*6/17-textWidth/2);
float textY= (float) (width/2 + Math.cos(2*Math.PI/360*(90-360*(getSum(i-1)+randomNums[i]/2)/sum))*width*6/17+textHeight/2);
/**
*百分比
* @param canvas
*/
private void drawText(Canvas canvas) {
if(sum==0) return;
paintText.setTextSize(width/30);
paintText.setAlpha((int)(255*value));
for (int i = 0; i < randomNums.length; i++) {
Rect rect = new Rect();
double f = (double)randomNums[i]/sum;
BigDecimal b = new BigDecimal(f*100);
double f1 = b.setScale(2,BigDecimal.ROUND_HALF_UP).doubleValue();
String textPercent=(f1)+"%";
paintText.getTextBounds(textPercent, 0, textPercent.length(), rect);
int textWidth = rect.width();
int textHeight = rect.height();
float textX= (float) (width/2 + Math.sin(2*Math.PI/360*(90-360*(getSum(i-1)+randomNums[i]/2)/sum))*width*6/17-textWidth/2);
float textY= (float) (width/2 + Math.cos(2*Math.PI/360*(90-360*(getSum(i-1)+randomNums[i]/2)/sum))*width*6/17+textHeight/2);
canvas.drawText(textPercent,textX,textY,paintText);
}
}
GitHub地址 https://github.com/ZuoJinDong/Chart