MPAndroidChart之PieChart源码分析
2017-05-08 16:44
295 查看
目前自己的项目用到图表。去github上看到MPAndroidChart很受欢迎,就下载下来了用了,随着项目的迭代,有些本身不具备的需求就来了。所以就花时间看了一下他的代码。非常感谢几个同事的帮忙。
根据时序图我把这个PieChart模块分成数据加载(1),图表参数准备(2,3,4,5,8),图表绘制(6,7,9,10)三个部分
![](https://img-blog.csdn.net/20170511173055186?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQTc3MTY0Mg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
(2)绘制CD弧边
(3)绘制DE线段
(4)绘制EB弧边
他这样写是有一点小bug。我把他的项目拆解学习运行时发现Path是连贯的操作,就感觉是游戏 一笔画。所以BC线段和DE线段是不需要绘制的。
lineto代码可以省略。
以上我们就完成了PieChart静态布局。接下来在来看一下开局动画的实现,我也通过这个项目第一次了解Android属性动画的强大。
![](https://img-blog.csdn.net/20170511201300456?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQTc3MTY0Mg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
(2)设置时间补间器(这个大家可以百度一下。我觉得数学好的人这个可以玩的贼6)
(3)添加动画更新回调
(4)启动动画
(2)设置扇形实际角度乘以当前动画对象的属性phaseY
4000
数学玩的好的人秒懂,我自己看了一晚上没什么思路,我几个同事秒懂。扎心了,老铁。大体思路是这样:
(2)事件类型是按下时,获得开始角度
(3)事件类型是滑动时,获得当前角度,在减去开始按下的角度就是旋转角度
(4)通知view去执行ondraw方法。
(2)作者规则:所有角度都是从X轴正半轴出发(一定要按同一标准换算角度)
(3)我假设按下事件的坐标点都是X轴正半轴,旋转角度就都是move事件坐标的角度
注意:这里计算的角度是反余弦值!
第四象限: 90 - angle
第三象限: 90+angle
第二象限:90+angle
第一象限:360-angle+90
![](https://img-blog.csdn.net/20170514180209794?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQTc3MTY0Mg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
![](https://img-blog.csdn.net/20170514180223576?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQTc3MTY0Mg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
![](https://img-blog.csdn.net/20170514180218763?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQTc3MTY0Mg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
![](https://img-blog.csdn.net/20170514180214701?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQTc3MTY0Mg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
他用的是反正弦值。其实和MPAndroid作者一样,但是他带入的点坐标没有正负之分,导致需要判断象限。从这点来看作者的数学逻辑更为优秀一点
(2)计算开始旋转角度
(3)换算成x,y坐标
(2)初始化集合为0
(3)把down事件的点击时间和和坐标X,Y的角度组装成AngularVelocitySample对象存入集合中
(4)把move事件的点击时间和和坐标X,Y的角度组装成AngularVelocitySample对象存入集合中
(5)初始化加速角速度为0
(6)把up事件的点击时间和和坐标X,Y的角度组装成AngularVelocitySample对象存入集合中
(7)计算角速度
(8)通知computeScroll执行
(2)判断滚动方向是CW(顺时针)和CCW(逆时针)
(1)判断正反转
后一次的角度大于前一次的就是顺时针,反之则是逆时针。但是有两个特殊情况,反生在360和0度交界处。
lastAngle与firstAngle,判断结果(如下图)与实际情况相悖。所以就有了下面的逻辑。
![](https://img-blog.csdn.net/20170515103707756?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQTc3MTY0Mg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
![](https://img-blog.csdn.net/20170515103715843?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQTc3MTY0Mg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
(2)角度修正
作者认为两者超过180度以后,换算的速度过大,所以就取∠1的补角∠2如下图:
![](https://img-blog.csdn.net/20170515103727554?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvQTc3MTY0Mg==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
(2)把计算出来的速度乘以大于0小于1的系数。
(3)计算时间差,换算成秒
(4)设置旋转角
(5)当速度大于0.01继续通知执行computeScroll。这个方法执行完毕会去执行ondraw方法,重新绘制view
一 图表绘制(方法10)
绘制方法基本都一样,就主要介绍一下绘制扇形区域。作者的步骤
(1)移动到B点(2)绘制CD弧边
(3)绘制DE线段
(4)绘制EB弧边
mPathBuffer.moveTo(arcStartPointX, arcStartPointY); mPathBuffer.arcTo( circleBox, startAngleOuter, sweepAngleOuter); mPathBuffer.lineTo( center.x + innerRadius * (float) Math.cos(endAngleInner * Utils.FDEG2RAD), center.y + innerRadius * (float) Math.sin(endAngleInner * Utils.FDEG2RAD)); mPathBuffer.arcTo( mInnerRectBuffer, endAngleInner, -sweepAngleInner);
他这样写是有一点小bug。我把他的项目拆解学习运行时发现Path是连贯的操作,就感觉是游戏 一笔画。所以BC线段和DE线段是不需要绘制的。
lineto代码可以省略。
以上我们就完成了PieChart静态布局。接下来在来看一下开局动画的实现,我也通过这个项目第一次了解Android属性动画的强大。
二 属性动画实现
1 初始化时序图
2 调用此方法执行开始动画
(1)创建属性动画对象(2)设置时间补间器(这个大家可以百度一下。我觉得数学好的人这个可以玩的贼6)
(3)添加动画更新回调
(4)启动动画
public void animateY(int durationMillis, Easing.EasingOption easing) { if (android.os.Build.VERSION.SDK_INT < 11) return; ObjectAnimator animatorY = ObjectAnimator.ofFloat(this, "phaseY", 0f, 1f); animatorY.setInterpolator(Easing.getEasingFunctionFromOption(easing)); animatorY.setDuration(durationMillis); animatorY.addUpdateListener(mListener); animatorY.start(); }
三 监听到动画更新回调时。调用方法postInvaliadate()去让PieChart执行ondraw()方法
(1) 获取时序图中方法6 创建对象时传入的动画对象的phaseY的属性值(2)设置扇形实际角度乘以当前动画对象的属性phaseY
float sliceAngle = drawAngles[j]; float phaseY = mAnimator.getPhaseY(); float sweepAngleOuter = (sliceAngle - sliceSpaceAngleOuter) * phaseY; float sweepAngleInner = (sliceAngle - sliceSpaceAngleInner) * phaseY;当动画更新回调AnimatorUpdateListener被不断触发时。就不断通知View执行ondraw,sweepAngle不断改变,达到动画效果。
问题
每次回调被触发时mAnimator.getPhaseY()是有不同的值的,有getPhaseY()那么一定会有方法去SetPhaseY()。在ChartAnimator.java中我确实发现了setPhaseY()。但是我全局搜索这个方法并没有发现有人去调用这个方法。到底是谁在调用这个方法?回答
(1)这个我没有去细看的原理,google写的注释很明确(this是ChartAnimator,propertyName是phaseY)这个对象应该有一个公共方法称为setName在其中。我猜测使用了反射。/** * Constructs and returns an ObjectAnimator that animates between float values. A single * value implies that that value is the one being animated to. Two values imply starting * and ending values. More than two values imply a starting value, values to animate through * along the way, and an ending value (these values will be distributed evenly across * the duration of the animation). * * @param target The object whose property is to be animated. This object should * have a public method on it called <code>setName()</code>, where <code>name</code> is * the value of the <code>propertyName</code> parameter. * @param propertyName The name of the property being animated. * @param values A set of values that the animation will animate between over time. * @return An ObjectAnimator object that is set up to animate between the given values. */ public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) { ObjectAnimator anim = new ObjectAnimator(target, propertyName); anim.setFloatValues(values); return anim; }
四 手势触摸滚动
看这个真的太受打击了,4000
数学玩的好的人秒懂,我自己看了一晚上没什么思路,我几个同事秒懂。扎心了,老铁。大体思路是这样:
1 touch事件监听
(1)监听onTouch事件。(2)事件类型是按下时,获得开始角度
(3)事件类型是滑动时,获得当前角度,在减去开始按下的角度就是旋转角度
(4)通知view去执行ondraw方法。
public boolean onTouch(View v, MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: setGestureStartAngle(x, y); break; case MotionEvent.ACTION_MOVE: updateGestureRotation(x, y); mChart.invalidate(); break; } return true; }
4 同一标准换算坐标
1 前提
(1)作者是以圆心为坐标原点,向上为Y轴正半轴,向右为X轴正半轴建立坐标系。(2)作者规则:所有角度都是从X轴正半轴出发(一定要按同一标准换算角度)
(3)我假设按下事件的坐标点都是X轴正半轴,旋转角度就都是move事件坐标的角度
public void updateGestureRotation(float x, float y) { /*Log.e("wjq","mChart.getAngleForPoint(x, y) = " + mChart.getAngleForPoint(x, y)); Log.e("wjq","mStartAngle = " + mStartAngle);*/ mChart.setRotationAngle(mChart.getAngleForPoint(x, y) - mStartAngle); }
注意:这里计算的角度是反余弦值!
public float getAngleForPoint(float x, float y) { MPPointF c = getCenterOffsets(); double tx = x - c.x, ty = y - c.y; double length = Math.sqrt(tx * tx + ty * ty); double r = Math.acos(ty / length); float angle = (float) Math.toDegrees(r); if (x > c.x) angle = 360f - angle; // add 90° because chart starts EAST angle = angle + 90f; // neutralize overflow if (angle > 360f) angle = angle - 360f; MPPointF.recycleInstance(c); return angle; }
2 角度计算
坐标点落四个象限时,所求角如下图示(角1为所求值,角2为反余弦值):第四象限: 90 - angle
第三象限: 90+angle
第二象限:90+angle
第一象限:360-angle+90
3 zhanghongyang的逻辑判断
http://blog.csdn.net/lmj623565791/article/details/43131133他用的是反正弦值。其实和MPAndroid作者一样,但是他带入的点坐标没有正负之分,导致需要判断象限。从这点来看作者的数学逻辑更为优秀一点
if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4) { mStartAngle += end - start; mTmpAngle += end - start; } else // 二、三象限,色角度值是付值 { mStartAngle += start - end; mTmpAngle += start - end; }
4 换算旋转角度
(1)获取旋转角度(2)计算开始旋转角度
(3)换算成x,y坐标
protected void drawDataSet(Canvas c, IPieDataSet dataSet) { float rotationAngle = mChart.getRotationAngle(); final float startAngleOuter = rotationAngle + (angle + sliceSpaceAngleOuter / 2.f) * phaseY; float arcStartPointX = center.x + radius * (float) Math.cos(startAngleOuter * Utils.FDEG2RAD); float arcStartPointY = center.y + radius * (float) Math.sin(startAngleOuter * Utils.FDEG2RAD); }
5 图表绘制(同上逻辑)
当监听时间move的x,y坐标在不断改变,旋转角度不断改变,通知view不断绘制,就达到了手势触摸旋转效果。五 惯性滚动
通过角加速度匀速减小角度,达到惯性滚动的目的。1 计算角速度
(1)初始化角加速度为0(2)初始化集合为0
(3)把down事件的点击时间和和坐标X,Y的角度组装成AngularVelocitySample对象存入集合中
(4)把move事件的点击时间和和坐标X,Y的角度组装成AngularVelocitySample对象存入集合中
(5)初始化加速角速度为0
(6)把up事件的点击时间和和坐标X,Y的角度组装成AngularVelocitySample对象存入集合中
(7)计算角速度
(8)通知computeScroll执行
public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: stopDeceleration(); resetVelocity(); sampleVelocity(x, y); break; case MotionEvent.ACTION_MOVE: sampleVelocity(x, y); break; case MotionEvent.ACTION_UP: stopDeceleration(); sampleVelocity(x, y); mDecelerationAngularVelocity = calculateVelocity(); mDecelerationLastTime = AnimationUtils.currentAnimationTimeMillis(); Utils.postInvalidateOnAnimation(mChart); break; } }
2 检查对象
这个方法要注意他检查的是索引第0个到倒数第三个,是为了保证calculateVelocity方法中必定要取到两个对象计算角加速度private void sampleVelocity(float touchLocationX, float touchLocationY) { long currentTime = AnimationUtils.currentAnimationTimeMillis(); _velocitySamples.add(new AngularVelocitySample(currentTime, mChart.getAngleForPoint(touchLocationX, touchLocationY))); // Remove samples older than our sample time - 1 seconds for (int i = 0, count = _velocitySamples.size(); i < count - 2; i++) { if (currentTime - _velocitySamples.get(i).time > 1000) { _velocitySamples.remove(0); i--; count--; } else { break; } } }
3 计算速度
(1)获得时间差和角度差,计算角加速度(2)判断滚动方向是CW(顺时针)和CCW(逆时针)
private float calculateVelocity() { if (_velocitySamples.isEmpty()) return 0.f; AngularVelocitySample firstSample = _velocitySamples.get(0); AngularVelocitySample lastSample = _velocitySamples.get(_velocitySamples.size() - 1); // Look for a sample that's closest to the latest sample, but not the same, so we can deduce the direction AngularVelocitySample beforeLastSample = firstSample; for (int i = _velocitySamples.size() - 1; i >= 0; i--) { beforeLastSample = _velocitySamples.get(i); if (beforeLastSample.angle != lastSample.angle) { break; } } // Calculate the sampling time float timeDelta = (lastSample.time - firstSample.time) / 1000.f; if (timeDelta == 0.f) { timeDelta = 0.1f; } // Calculate clockwise/ccw by choosing two values that should be closest to each other, // so if the angles are two far from each other we know they are inverted "for sure" boolean clockwise = lastSample.angle >= beforeLastSample.angle; if (Math.abs(lastSample.angle - beforeLastSample.angle) > 270.0) { clockwise = !clockwise; } // Now if the "gesture" is over a too big of an angle - then we know the angles are inverted, and we need to move them closer to each other from both sides of the 360.0 wrapping point if (lastSample.angle - firstSample.angle > 180.0) { firstSample.angle += 360.0; } else if (firstSample.angle - lastSample.angle > 180.0) { lastSample.angle += 360.0; } // The velocity float velocity = Math.abs((lastSample.angle - firstSample.angle) / timeDelta); // Direction? if (!clockwise) { velocity = -velocity; } return velocity; }
(1)判断正反转
后一次的角度大于前一次的就是顺时针,反之则是逆时针。但是有两个特殊情况,反生在360和0度交界处。
lastAngle与firstAngle,判断结果(如下图)与实际情况相悖。所以就有了下面的逻辑。
(2)角度修正
作者认为两者超过180度以后,换算的速度过大,所以就取∠1的补角∠2如下图:
3 执行滚动
(1)获取当前时间(2)把计算出来的速度乘以大于0小于1的系数。
(3)计算时间差,换算成秒
(4)设置旋转角
(5)当速度大于0.01继续通知执行computeScroll。这个方法执行完毕会去执行ondraw方法,重新绘制view
public void computeScroll() { if (mDecelerationAngularVelocity == 0.f) return; // There's no deceleration in progress final long currentTime = AnimationUtils.currentAnimationTimeMillis(); mDecelerationAngularVelocity *= mChart.getDragDecelerationFrictionCoef(); final float timeInterval = (float) (currentTime - mDecelerationLastTime) / 1000.f; mChart.setRotationAngle(mChart.getRotationAngle() + mDecelerationAngularVelocity * timeInterval); mDecelerationLastTime = currentTime; if (Math.abs(mDecelerationAngularVelocity) >= 0.001) Utils.postInvalidateOnAnimation(mChart); // This causes computeScroll to fire, recommended for this by Google else stopDeceleration(); }
相关文章推荐
- Android MPAndroidChart使用教程和源码分析(二)
- Android MPAndroidChart使用教程和源码分析(五)
- MPAndroidChart之LineChart源码分析
- MPAndroidChart系列源码解读(一)
- MPAndroidChart系列源码解读(二)
- MPAndroidChart系列源码解读(四)
- Android MPAndroidChart之PieChart和数据结构以及模型【5】
- MPAndroidChart之PieChart
- MPAndroidChart系列源码解读(五)
- MPAndroidChart之PieChart
- MpAndroidChart源码修改之饼状图添加标志线
- MPAndroidChart饼图PieChart
- Android MPAndroidChart之PieChart和数据结构以及模型【5】
- MPAndroidChart系列源码解读(三)
- ArcGIS runtime sdk for android 结合mpchartlib进行离线数据统计分析
- MPAndroidChat的PieChart和BarChart学习
- Android IPC 通讯机制源码分析
- Android App 源码分析(贪吃蛇游戏)