说说Android桌面(Launcher应用)背后的故事(七)——又是一个附属品(可以转动的绚烂饼图)

来源:互联网 发布:视频压缩算法有哪些 编辑:程序博客网 时间:2024/04/27 18:58

 本来这一篇应该还是写Launcher中item拖拽的实现原理的,奈何,自从研究了Launcher,以前没有实现的,现在灵感全来了。这不,一个月前看到了著名记账软件随手记,看到android版中有一个炫酷的可以旋转的统计饼图,当时,下载了APK,反编译了下,奈何,不知道是不是在代码中进行了处理,没有反编译出源码来,半点都没有。只反编译成功了资源文件。当时,这个事情就放下了,虽然心很有不甘。但是,网上也没有看到有人实现,只能作罢。

可是当研究完了Launcher之后,再来考量一下其实现原理,竟然恍然大悟般,于是乎,今天一天的时间,终于实现了其一样的功能。我现在倒觉得,分析了Launcher源码之后,让我对自定义控件倒是有了很多新的认识。

废话不说了,先来看下随手记的效果图:

 

怎么样很炫吧?而且手指可以将饼图任意的转动哦。当时看到的时候,就眼睛一亮,很想自己实现。现在,就再来看看俺实现的效果,虽然实现了一样的功能,但是感觉颜色没有它那么亮。

 

反编译后只能看到一张图片:

 

 

其实,你别小看这么一张图片,这张图片的制作者,我真的佩服的要死,我的ps技术要是有他一半精湛,我就可以靠ps吃饭了。哎,所以,就先拿来使用了。下面就直接贴代码了,具体的代码中注释的清清楚楚。

 

[java] view plaincopyprint?
  1. /** 
  2.  * 随手记中可以任意旋转的炫酷饼图的实现原理 
  3.  *  
  4.  * 小记: 
  5.  * 在实现的过程中,主要是用到了一些数学计算来实现角度和屏幕位置坐标的计算 
  6.  * 关于任意两个点之间的角度计算的问题,一开始想了很久没有结果,最后,一个偶然的灵光,让整个 
  7.  * 事情变得简单起来,就是计算任意两个点相对于中心坐标的角度的时候,首先,计算 
  8.  * 每个点相对于x轴正方向的角度,这样,总可以将其转化为计算直角三角形的内角计算问题 
  9.  * 再将两次计算的角度进行减法运算,就实现了。是不是很简单?呵呵,对于像我们这样数学 
  10.  * 没有学好的开发者来说,也只有这样化难为简了 
  11.  *  
  12.  * @author liner 
  13.  * 
  14.  */  
  15. public class PieChart extends View{  
  16.     public static final String TAG = "PieChart";  
  17.     public static final int ALPHA = 100;  
  18.     public static final int ANIMATION_DURATION = 800;  
  19.     public static final int ANIMATION_STATE_RUNNING = 1;  
  20.     public static final int ANIMATION_STATE_DOWN = 2;  
  21.     /** 
  22.      * 不要问我这个值是怎么设置的。这个就是图片中的一大块圆形区域对应的长方形四个边的坐标位置 
  23.      * 具体的值,自己需要多次尝试并调整了。这样,我们的饼图就是相对于这个区域来画的 
  24.      */  
  25.     private static final RectF OVAL = new RectF(18,49,303,340);  
  26.   
  27.       
  28.     private int[] colors; //每部分的颜色值  
  29.       
  30.     private int[] values; //每部分的大小  
  31.       
  32.     private int[] degrees; //值转换成角度  
  33.       
  34.     private String[] titles; //每部分的内容  
  35.       
  36.     private Paint paint;  
  37.       
  38.     private Paint maskPaint;  
  39.       
  40.     private Paint textPaint;  
  41.       
  42.     private Point lastEventPoint;  
  43.       
  44.     private int currentTargetIndex = -1;  
  45.       
  46.     private Point center; //这个是饼图的中心位置  
  47.       
  48.     private int eventRadius = 0//事件距离饼图中心的距离  
  49.       
  50.     //测试的时候使用的   
  51.     //private ChartClickListener clickListener;  
  52.       
  53.     private Bitmap mask; //用于遮罩的Bitmap  
  54.       
  55.     private int startDegree = 90//让初始的时候,圆饼是从箭头位置开始画出的  
  56.       
  57.       
  58.     private int animState = ANIMATION_STATE_DOWN;  
  59.       
  60.     private boolean animEnabled = false;  
  61.       
  62.     private long animStartTime;  
  63.       
  64.     public PieChart(Context context) {  
  65.         super(context);  
  66.         init();  
  67.     }  
  68.       
  69.     public PieChart(Context context, AttributeSet attrs){  
  70.         this(context, attrs, 0);  
  71.     }  
  72.       
  73.     public PieChart(Context context, AttributeSet attrs, int defStyle){  
  74.         super(context, attrs, defStyle);  
  75.         init();  
  76.     }  
  77.       
  78.     private void init(){  
  79.         paint = new Paint();  
  80.           
  81.         maskPaint = new Paint();  
  82.           
  83.           
  84.         textPaint = new Paint();  
  85.         textPaint.setColor(Color.WHITE);  
  86.         textPaint.setTypeface(Typeface.DEFAULT_BOLD);  
  87.         textPaint.setAlpha(100);  
  88.         textPaint.setTextSize(16);  
  89.           
  90.         values = new int[]{  
  91.                 60,  
  92.                 90,  
  93.                 30,  
  94.                 50,  
  95.                 70  
  96.         };  
  97.           
  98. //      titles = new String[]{   
  99. //              "川菜",   
  100. //              "徽菜",   
  101. //              "粤菜",   
  102. //              "闽菜",   
  103. //              "湘菜"   
  104. //      };   
  105.         //测试文字居中显示   
  106.         titles = new String[]{  
  107.                 "我是三岁",  
  108.                 "说了算四大皆空",  
  109.                 "士大",  
  110.                 "史蒂芬森地",  
  111.                 "湘"  
  112.         };        
  113.           
  114.         colors = new int[]{  
  115.             Color.argb(ALPHA, 2496464),  
  116.             Color.argb(ALPHA, 02550),  
  117.             Color.argb(ALPHA, 2550255),  
  118.             Color.argb(ALPHA, 2552550),  
  119.             Color.argb(ALPHA, 0255255)  
  120.         };  
  121.           
  122.         degrees = getDegrees();  
  123.           
  124.         //Drawable d = getResources().getDrawable(R.drawable.mask);  
  125.         mask = BitmapFactory.decodeResource(getResources(), R.drawable.mask);  
  126.           
  127.         //获取初始位置的时候,下方箭头所在的区域         
  128.         animEnabled = true//同时,启动动画  
  129.     }  
  130.       
  131. //  public void setChartClickListener(ChartClickListener l){  
  132. //      this.clickListener = l;   
  133. //  }   
  134.       
  135.     //计算总和   
  136.     private int sum(int[] values){  
  137.         int sum = 0;  
  138.         for(int i=0; i<values.length;i++){  
  139.             sum += values[i];  
  140.         }  
  141.           
  142.         return sum;  
  143.     }  
  144.       
  145.     /** 
  146.      * 根据每部分所占的比例,来计算每个区域在整个圆中所占的角度 
  147.      * 但是,有个小细节,就是计算的时候注意,可能并不能整除的情况,这个时候,为了 
  148.      * 避免所有的角度和小于360度的情况,姑且将剩余的部分送给某个部分,反正也不影响 
  149.      * @return 
  150.      */  
  151.     private int[] getDegrees(){  
  152.         int sum = this.sum(values);  
  153.           
  154.         int[] degrees = new int[values.length];  
  155.         for(int i=0; i<values.length; i++){  
  156.             degrees[i] = (int)Math.floor((double)((double)values[i]/(double)sum)*360);  
  157.             //Log.v("Angle", angles[i]+"");  
  158.         }  
  159.         int angleSum = this.sum(degrees);  
  160.         if(angleSum != 360){  
  161.             //上面的计算可能导致和小于360   
  162.             int c = 360 - angleSum;  
  163.             degrees[values.length-1] += c; //姑且让最后一个的值稍大点  
  164.         }  
  165.           
  166.         return degrees;  
  167.     }  
  168.       
  169.     /** 
  170.      * 重写这个方法来画出整个界面 
  171.      */  
  172.     protected void onDraw(Canvas canvas) {  
  173.         super.onDraw(canvas);  
  174.   
  175.         if(animEnabled){  
  176.             /** 
  177.              * 说明是启动的时候,需要旋转着画出饼图 
  178.              */  
  179.             Log.e(TAG, "anim enabled");  
  180.             if(animState == ANIMATION_STATE_DOWN){  
  181.   
  182.                 animStartTime = SystemClock.uptimeMillis();  
  183.                 animState = ANIMATION_STATE_RUNNING;  
  184.                   
  185.             }  
  186.                   
  187.             final long currentTimeDiff = SystemClock.uptimeMillis() - animStartTime;  
  188.             int currentMaxDegree = (int)((float)currentTimeDiff/ANIMATION_DURATION*360f);  
  189.               
  190.             Log.e(TAG, "当前最大的度数为:"+currentMaxDegree);  
  191.               
  192.             if(currentMaxDegree >= 360){  
  193.                 //动画结束状态,停止绘制   
  194.                 currentMaxDegree = 360;  
  195.                 animState = ANIMATION_STATE_DOWN;  
  196.                 animEnabled = false;  
  197.             }  
  198.               
  199.             int[] degrees = getDegrees();  
  200.             int startAngle = this.startDegree;  
  201.               
  202.             //获取当前时刻最大可以旋转的角度所位于的区域   
  203.             int maxIndex = getEventPart(currentMaxDegree);  
  204.               
  205.             //根据不同的颜色画饼图   
  206.             for(int i=0; i<= maxIndex; i++){  
  207.                 int currentDegree = degrees[i];  
  208.                   
  209.                 if(i== maxIndex){  
  210.                     //对于当前最后一个绘制区域,可能只是一部分,需要获取其偏移量  
  211.                     currentDegree = getOffsetOfPartStart(currentMaxDegree, maxIndex);  
  212.                 }  
  213.                   
  214.                 if(i > 0){  
  215.                     //注意,每次画饼图,记得计算startAngle  
  216.                     startAngle += degrees[i-1];  
  217.                 }  
  218.                   
  219.                 paint.setColor(colors[i]);  
  220.                 canvas.drawArc(OVAL, startAngle, currentDegree, true, paint);  
  221.             }  
  222.               
  223.             if(animState == ANIMATION_STATE_DOWN){  
  224.   
  225.                 //如果动画结束了,则调整当前箭头位于所在区域的中心方向  
  226.                 onStop();  
  227.                   
  228.             }else{  
  229.                 postInvalidate();      
  230.             }     
  231.               
  232.         }else{  
  233.   
  234.             int[] degrees = getDegrees();  
  235.             int startAngle = this.startDegree;  
  236.               
  237.             /** 
  238.              * 每个区域的颜色不同,但是这里只要控制好每个区域的角度就可以了,整个是个圆 
  239.              */  
  240.             for(int i=0; i<values.length; i++){  
  241.                 paint.setColor(colors[i]);  
  242.                 if(i>0){  
  243.                     startAngle += degrees[i-1];  
  244.                 }  
  245.                 canvas.drawArc(OVAL, startAngle, degrees[i], true, paint);  
  246.   
  247.             }             
  248.         }  
  249.           
  250.           
  251.         /** 
  252.          * 画出饼图之后,画遮罩图片,这样图片就位于饼图之上了,形成了遮罩的效果 
  253.          */  
  254.         canvas.drawBitmap(mask, 00, maskPaint);  
  255.           
  256.         /** 
  257.          * 根据当前计算得到的箭头所在区域显示该区域代表的信息 
  258.          */  
  259.         if(currentTargetIndex >= 0){  
  260.             String title = titles[currentTargetIndex];  
  261.             textPaint.setColor(colors[currentTargetIndex]);  
  262.             //简单作个计算,让文字居中显示   
  263.             int width = title.length()*17;  
  264.             canvas.drawText(title, 157-width/2+3383, textPaint);  
  265.         }  
  266.   
  267.     }  
  268.   
  269.       
  270.     /** 
  271.      * 处理饼图的转动 
  272.      */  
  273.     public boolean onTouchEvent(MotionEvent event){  
  274.           
  275.         if(animEnabled && animState == ANIMATION_STATE_RUNNING){  
  276.             return super.onTouchEvent(event);  
  277.         }  
  278.           
  279.         Point eventPoint = getEventAbsoluteLocation(event);  
  280.         computeCenter(); //计算中心坐标   
  281.           
  282.         //计算当前位置相对于x轴正方向的角度   
  283.         //在下面这个方法中计算了eventRadius的  
  284.         int newAngle = getEventAngle(eventPoint, center);   
  285.           
  286.         int action = event.getAction();  
  287.           
  288.         switch (action) {  
  289.         case MotionEvent.ACTION_DOWN:  
  290.               
  291.             lastEventPoint = eventPoint;  
  292.               
  293.             if(eventRadius > getRadius()){  
  294.                 /** 
  295.                  * 只有点在饼图内部才需要处理转动,否则直接返回 
  296.                  */  
  297.                 Log.e(TAG, "当前位置超出了半径:"+eventRadius+">"+getRadius());  
  298.                 return super.onTouchEvent(event);  
  299.             }  
  300.               
  301.             break;  
  302.         case MotionEvent.ACTION_MOVE:  
  303.             //这里处理滑动   
  304.             rotate(eventPoint, newAngle);  
  305.               
  306.             //处理之后,记得更新lastEventPoint   
  307.             lastEventPoint = eventPoint;  
  308.             break;  
  309.               
  310.         case MotionEvent.ACTION_UP:  
  311.             onStop();  
  312.             break;  
  313.   
  314.         default:  
  315.             break;  
  316.         }  
  317.           
  318.         return true;  
  319.     }      
  320.       
  321.     /** 
  322.      * 当我们停止旋转的时候,如果当前下方箭头位于某个区域的非中心位置,则我们需要计算 
  323.      * 偏移量,并且将箭头指向中心位置 
  324.      */  
  325.     private void onStop() {  
  326.           
  327.         int targetAngle = getTargetDegree();  
  328.         currentTargetIndex = getEventPart(targetAngle);  
  329.   
  330.         int offset = getOffsetOfPartCenter(targetAngle, currentTargetIndex);  
  331.           
  332.         /** 
  333.          * offset>0,说明当前箭头位于中心位置右边,则所有区域沿着顺时针旋转offset大小的角度 
  334.          * offset<0,正好相反 
  335.          */  
  336.         startDegree += offset;  
  337.           
  338.         postInvalidateDelayed(200);  
  339.     }  
  340.   
  341.     private void rotate(Point eventPoint, int newDegree) {  
  342.           
  343.         //计算上一个位置相对于x轴正方向的角度   
  344.         int lastDegree = getEventAngle(lastEventPoint, center);   
  345.           
  346.   
  347.         /** 
  348.          * 其实转动就是不断的更新画圆弧时候的起始角度,这样,每次从新的起始角度重画圆弧就形成了转动的效果 
  349.          */  
  350.         startDegree += newDegree-lastDegree;  
  351.           
  352.         //转多圈的时候,限定startAngle始终在-360-360度之间  
  353.         if(startDegree >= 360){  
  354.             startDegree -= 360;  
  355.         }else if(startDegree <= -360){  
  356.             startDegree += 360;  
  357.         }  
  358.           
  359.         Log.e(TAG, "当前startAngle:"+startDegree);  
  360.           
  361.         //获取当前下方箭头所在的区域,这样在onDraw的时候就会转到不同区域显示的是当前区域对应的信息  
  362.         int targetDegree = getTargetDegree();  
  363.         currentTargetIndex = getEventPart(targetDegree);          
  364.           
  365.         //请求重新绘制界面,调用onDraw方法   
  366.         postInvalidate();  
  367.   
  368.     }  
  369.       
  370.       
  371.     /** 
  372.      * 获取当前事件event相对于屏幕的坐标 
  373.      * @param event 
  374.      * @return 
  375.      */  
  376.     protected Point getEventAbsoluteLocation(MotionEvent event){  
  377.         int[] location = new int[2];  
  378.         this.getLocationOnScreen(location); //当前控件在屏幕上的位置      
  379.           
  380.         int x = (int)event.getX();  
  381.         int y = (int)event.getY();  
  382.           
  383.         x += location[0];  
  384.         y += location[1]; //这样x,y就代表当前事件相对于整个屏幕的坐标  
  385.           
  386.         Point p = new Point(x, y);  
  387.           
  388.         Log.v(TAG, "事件坐标:"+p.toString());  
  389.           
  390.         return p;  
  391.     }  
  392.       
  393.     /** 
  394.      * 获取当前饼图的中心坐标,相对于屏幕左上角 
  395.      */  
  396.     protected void computeCenter(){  
  397.         if(center == null){  
  398.             int x = (int)OVAL.left + (int)((OVAL.right-OVAL.left)/2f);  
  399.             int y = (int)OVAL.top + (int)((OVAL.bottom - OVAL.top)/2f)+50//状态栏的高度是50  
  400.             center = new Point(x,y);  
  401.             //Log.v(TAG, "中心坐标:"+center.toString());              
  402.         }  
  403.     }  
  404.       
  405.     /** 
  406.      * 获取半径 
  407.      */  
  408.     protected int getRadius(){  
  409.         int radius = (int)((OVAL.right-OVAL.left)/2f);  
  410.         //Log.v(TAG, "半径:"+radius);   
  411.         return radius;  
  412.     }  
  413.       
  414.     /** 
  415.      * 获取事件坐标相对于饼图的中心x轴正方向的角度 
  416.      * 这里就是坐标系的转换,本例中使用饼图的中心作为坐标中心,就是我们从初中到大学一直使用的"正常"坐标系。 
  417.      * 但是涉及到圆的转动,本例中一律相对于x正方向顺时针来计算某个事件在坐标系中的位置 
  418.      * @param eventPoint 
  419.      * @param center 
  420.      * @return 
  421.      */  
  422.     protected int getEventAngle(Point eventPoint, Point center){  
  423.         int x = eventPoint.x - center.x;//x轴方向的偏移量  
  424.         int y = eventPoint.y - center.y; //y轴方向的偏移量  
  425.           
  426.         //Log.v(TAG, "直角三角形两直边长度:"+x+","+y);  
  427.           
  428.         double z = Math.hypot(Math.abs(x), Math.abs(y)); //求直角三角形斜边的长度  
  429.           
  430.         //Log.v(TAG, "斜边长度:"+z);   
  431.           
  432.         eventRadius = (int)z;  
  433.         double sinA = (double)Math.abs(y)/z;  
  434.           
  435.         //Log.v(TAG, "sinA="+sinA);  
  436.           
  437.         double asin = Math.asin(sinA); //求反正玄,得到当前点和x轴的角度,是最小的那个  
  438.           
  439.         //Log.v(TAG, "当前相对偏移角度的反正弦:"+asin);  
  440.           
  441.         int degree = (int)(asin/3.14f*180f);  
  442.           
  443.         //Log.v(TAG, "当前相对偏移角度:"+angle);  
  444.           
  445.         //下面就需要根据x,y的正负,来判断当前点和x轴的正方向的夹角  
  446.         int realDegree = 0;  
  447.         if(x<=0 && y<=0){  
  448.             //左上方,返回180+angle   
  449.               
  450.             realDegree = 180+degree;  
  451.               
  452.         }else if(x>=0 && y<=0){  
  453.             //右上方,返回360-angle   
  454.             realDegree = 360-degree;  
  455.         }else if(x<=0 && y>=0){  
  456.             //左下方,返回180-angle   
  457.             realDegree = 180-degree;  
  458.         }else{  
  459.             //右下方,直接返回   
  460.             realDegree = degree;  
  461.               
  462.         }  
  463.           
  464.         //Log.v(TAG, "当前事件相对于中心坐标x轴正方形的顺时针偏移角度为:"+realAngle);  
  465.           
  466.         return realDegree;  
  467.   
  468.     }  
  469.       
  470.     /** 
  471.      * 获取当前下方箭头位置相对于startDegree的角度值 
  472.      * 注意,下方箭头相对于x轴正方向是90度 
  473.      * @return 
  474.      */  
  475.     protected int getTargetDegree(){  
  476.           
  477.         int targetDegree = -1;  
  478.           
  479.         int tmpStart = startDegree;  
  480.           
  481.         /** 
  482.          * 如果当前startAngle为负数,则直接+360,转换为正值 
  483.          */  
  484.         if(tmpStart < 0){  
  485.             tmpStart += 360;  
  486.         }  
  487.           
  488.           
  489.         if(tmpStart < 90){  
  490.             /** 
  491.              * 如果startAngle小于90度(可能为负数) 
  492.              */  
  493.             targetDegree = 90 - tmpStart;  
  494.         }else{  
  495.             /** 
  496.              * 如果startAngle大于90,由于在每次计算startAngle的时候,限定了其最大为360度,所以 
  497.              * 直接可以按照如下公式计算 
  498.              */  
  499.             targetDegree = 360 + 90 - tmpStart;  
  500.         }  
  501.           
  502.         //Log.e(TAG, "Taget Angle:"+targetDegree+"startAngle:"+startAngle);  
  503.           
  504.         return targetDegree;  
  505.     }  
  506.       
  507.     /** 
  508.      *判断角度为degree坐落在饼图的哪个部分 
  509.      *注意,这里的角度一定是正值,而且不是相对于x轴正方向,而是相对于startAngle 
  510.      *返回当前部分的索引 
  511.      * @param degree 
  512.      * @return 
  513.      */  
  514.     protected int getEventPart(int degree){  
  515.         int currentSum = 0;  
  516.           
  517.         for(int i=0; i<degrees.length; i++){  
  518.             currentSum += degrees[i];  
  519.             if(currentSum >= degree){  
  520.                 return i;  
  521.             }  
  522.         }  
  523.           
  524.         return -1;  
  525.     }  
  526.       
  527.     /** 
  528.      * 在已经得知了当前degree位于targetIndex区域的情况下,计算angle相对于区域targetIndex起始位置的偏移量 
  529.      * @param degree 
  530.      * @param targetIndex 
  531.      * @return 
  532.      */  
  533.     protected int getOffsetOfPartStart(int degree, int targetIndex){  
  534.         int currentSum = 0;  
  535.         for(int i=0; i<targetIndex; i++){  
  536.             currentSum += degrees[i];  
  537.         }  
  538.           
  539.         int offset = degree - currentSum;  
  540.           
  541.         return offset;        
  542.     }  
  543.       
  544.     /** 
  545.      * 在已经得知了当前degree位于targetIndex区域的情况下,计算angle相对于区域targetIndex中心位置的偏移量 
  546.      * 这个是当我们停止旋转的时候,通过计算偏移量,来使得箭头指向当前区域的中心位置 
  547.      * @param degree 
  548.      * @param targetIndex 
  549.      * @return 
  550.      */  
  551.     protected int getOffsetOfPartCenter(int degree, int targetIndex){  
  552.         int currentSum = 0;  
  553.         for(int i=0; i<=targetIndex; i++){  
  554.             currentSum += degrees[i];  
  555.         }  
  556.           
  557.         int offset = degree - (currentSum-degrees[targetIndex]/2);   
  558.           
  559.         //超过一半,则offset>0;未超过一半,则offset<0  
  560.         return offset;  
  561.     }  
  562.       
  563.   
  564. }  

程序中用到的Point对象:

[java] view plaincopyprint?
  1. public class Point {  
  2.       
  3.     public int x;  
  4.     public int y;  
  5.       
  6.       
  7.     public Point(int x, int y){  
  8.         this.x = x;  
  9.         this.y = y;  
  10.     }  
  11.       
  12.     public int[] getPoint(){  
  13.         int[] point = new int[2];  
  14.         point[0] = x;  
  15.         point[1] = y;  
  16.           
  17.         return point;  
  18.     }  
  19.       
  20.     public String toString(){  
  21.           
  22.         return new StringBuilder("[").append(x).append(",").append(y).append("]").toString();  
  23.     }  
  24.   
  25. }  


原创粉丝点击