本来这一篇应该还是写Launcher中item拖拽的实现原理的,奈何,自从研究了Launcher,以前没有实现的,现在灵感全来了。这不,一个月前看到了著名记账软件随手记,看到android版中有一个炫酷的可以旋转的统计饼图,当时,下载了APK,反编译了下,奈何,不知道是不是在代码中进行了处理,没有反编译出源码来,半点都没有。只反编译成功了资源文件。当时,这个事情就放下了,虽然心很有不甘。但是,网上也没有看到有人实现,只能作罢。 可是当研究完了Launcher之后,再来考量一下其实现原理,竟然恍然大悟般,于是乎,今天一天的时间,终于实现了其一样的功能。我现在倒觉得,分析了Launcher源码之后,让我对自定义控件倒是有了很多新的认识。 废话不说了,先来看下随手记的效果图:
程序中用到的Point对象:
/** * 随手记中可以任意旋转的炫酷饼图的实现原理 * * 小记: * 在实现的过程中,主要是用到了一些数学计算来实现角度和屏幕位置坐标的计算 * 关于任意两个点之间的角度计算的问题,一开始想了很久没有结果,最后,一个偶然的灵光,让整个 * 事情变得简单起来,就是计算任意两个点相对于中心坐标的角度的时候,首先,计算 * 每个点相对于x轴正方向的角度,这样,总可以将其转化为计算直角三角形的内角计算问题 * 再将两次计算的角度进行减法运算,就实现了。是不是很简单?呵呵,对于像我们这样数学 * 没有学好的开发者来说,也只有这样化难为简了 * * @author liner * */ public class PieChart extends View{ public static final String TAG = "PieChart"; public static final int ALPHA = 100; public static final int ANIMATION_DURATION = 800; public static final int ANIMATION_STATE_RUNNING = 1; public static final int ANIMATION_STATE_DOWN = 2; /** * 不要问我这个值是怎么设置的。这个就是图片中的一大块圆形区域对应的长方形四个边的坐标位置 * 具体的值,自己需要多次尝试并调整了。这样,我们的饼图就是相对于这个区域来画的 */ private static final RectF OVAL = new RectF(18,49,303,340); private int[] colors; //每部分的颜色值 private int[] values; //每部分的大小 private int[] degrees; //值转换成角度 private String[] titles; //每部分的内容 private Paint paint; private Paint maskPaint; private Paint textPaint; private Point lastEventPoint; private int currentTargetIndex = -1; private Point center; //这个是饼图的中心位置 private int eventRadius = 0; //事件距离饼图中心的距离 //测试的时候使用的 //private ChartClickListener clickListener; private Bitmap mask; //用于遮罩的Bitmap private int startDegree = 90; //让初始的时候,圆饼是从箭头位置开始画出的 private int animState = ANIMATION_STATE_DOWN; private boolean animEnabled = false; private long animStartTime; public PieChart(Context context) { super(context); init(); } public PieChart(Context context, AttributeSet attrs){ this(context, attrs, 0); } public PieChart(Context context, AttributeSet attrs, int defStyle){ super(context, attrs, defStyle); init(); } private void init(){ paint = new Paint(); maskPaint = new Paint(); textPaint = new Paint(); textPaint.setColor(Color.WHITE); textPaint.setTypeface(Typeface.DEFAULT_BOLD); textPaint.setAlpha(100); textPaint.setTextSize(16); values = new int[]{ 60, 90, 30, 50, 70 }; // titles = new String[]{ // "川菜", // "徽菜", // "粤菜", // "闽菜", // "湘菜" // }; //测试文字居中显示 titles = new String[]{ "我是三岁", "说了算四大皆空", "士大", "史蒂芬森地", "湘" }; colors = new int[]{ Color.argb(ALPHA, 249, 64, 64), Color.argb(ALPHA, 0, 255, 0), Color.argb(ALPHA, 255, 0, 255), Color.argb(ALPHA, 255, 255, 0), Color.argb(ALPHA, 0, 255, 255) }; degrees = getDegrees(); //Drawable d = getResources().getDrawable(R.drawable.mask); mask = BitmapFactory.decodeResource(getResources(), R.drawable.mask); //获取初始位置的时候,下方箭头所在的区域 animEnabled = true; //同时,启动动画 } // public void setChartClickListener(ChartClickListener l){ // this.clickListener = l; // } //计算总和 private int sum(int[] values){ int sum = 0; for(int i=0; i= 360){ //动画结束状态,停止绘制 currentMaxDegree = 360; animState = ANIMATION_STATE_DOWN; animEnabled = false; } int[] degrees = getDegrees(); int startAngle = this.startDegree; //获取当前时刻最大可以旋转的角度所位于的区域 int maxIndex = getEventPart(currentMaxDegree); //根据不同的颜色画饼图 for(int i=0; i<= maxIndex; i++){ int currentDegree = degrees[i]; if(i== maxIndex){ //对于当前最后一个绘制区域,可能只是一部分,需要获取其偏移量 currentDegree = getOffsetOfPartStart(currentMaxDegree, maxIndex); } if(i > 0){ //注意,每次画饼图,记得计算startAngle startAngle += degrees[i-1]; } paint.setColor(colors[i]); canvas.drawArc(OVAL, startAngle, currentDegree, true, paint); } if(animState == ANIMATION_STATE_DOWN){ //如果动画结束了,则调整当前箭头位于所在区域的中心方向 onStop(); }else{ postInvalidate(); } }else{ int[] degrees = getDegrees(); int startAngle = this.startDegree; /** * 每个区域的颜色不同,但是这里只要控制好每个区域的角度就可以了,整个是个圆 */ for(int i=0; i 0){ startAngle += degrees[i-1]; } canvas.drawArc(OVAL, startAngle, degrees[i], true, paint); } } /** * 画出饼图之后,画遮罩图片,这样图片就位于饼图之上了,形成了遮罩的效果 */ canvas.drawBitmap(mask, 0, 0, maskPaint); /** * 根据当前计算得到的箭头所在区域显示该区域代表的信息 */ if(currentTargetIndex >= 0){ String title = titles[currentTargetIndex]; textPaint.setColor(colors[currentTargetIndex]); //简单作个计算,让文字居中显示 int width = title.length()*17; canvas.drawText(title, 157-width/2+3, 383, textPaint); } } /** * 处理饼图的转动 */ public boolean onTouchEvent(MotionEvent event){ if(animEnabled && animState == ANIMATION_STATE_RUNNING){ return super.onTouchEvent(event); } Point eventPoint = getEventAbsoluteLocation(event); computeCenter(); //计算中心坐标 //计算当前位置相对于x轴正方向的角度 //在下面这个方法中计算了eventRadius的 int newAngle = getEventAngle(eventPoint, center); int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: lastEventPoint = eventPoint; if(eventRadius > getRadius()){ /** * 只有点在饼图内部才需要处理转动,否则直接返回 */ Log.e(TAG, "当前位置超出了半径:"+eventRadius+">"+getRadius()); return super.onTouchEvent(event); } break; case MotionEvent.ACTION_MOVE: //这里处理滑动 rotate(eventPoint, newAngle); //处理之后,记得更新lastEventPoint lastEventPoint = eventPoint; break; case MotionEvent.ACTION_UP: onStop(); break; default: break; } return true; } /** * 当我们停止旋转的时候,如果当前下方箭头位于某个区域的非中心位置,则我们需要计算 * 偏移量,并且将箭头指向中心位置 */ private void onStop() { int targetAngle = getTargetDegree(); currentTargetIndex = getEventPart(targetAngle); int offset = getOffsetOfPartCenter(targetAngle, currentTargetIndex); /** * offset>0,说明当前箭头位于中心位置右边,则所有区域沿着顺时针旋转offset大小的角度 * offset<0,正好相反 */ startDegree += offset; postInvalidateDelayed(200); } private void rotate(Point eventPoint, int newDegree) { //计算上一个位置相对于x轴正方向的角度 int lastDegree = getEventAngle(lastEventPoint, center); /** * 其实转动就是不断的更新画圆弧时候的起始角度,这样,每次从新的起始角度重画圆弧就形成了转动的效果 */ startDegree += newDegree-lastDegree; //转多圈的时候,限定startAngle始终在-360-360度之间 if(startDegree >= 360){ startDegree -= 360; }else if(startDegree <= -360){ startDegree += 360; } Log.e(TAG, "当前startAngle:"+startDegree); //获取当前下方箭头所在的区域,这样在onDraw的时候就会转到不同区域显示的是当前区域对应的信息 int targetDegree = getTargetDegree(); currentTargetIndex = getEventPart(targetDegree); //请求重新绘制界面,调用onDraw方法 postInvalidate(); } /** * 获取当前事件event相对于屏幕的坐标 * @param event * @return */ protected Point getEventAbsoluteLocation(MotionEvent event){ int[] location = new int[2]; this.getLocationOnScreen(location); //当前控件在屏幕上的位置 int x = (int)event.getX(); int y = (int)event.getY(); x += location[0]; y += location[1]; //这样x,y就代表当前事件相对于整个屏幕的坐标 Point p = new Point(x, y); Log.v(TAG, "事件坐标:"+p.toString()); return p; } /** * 获取当前饼图的中心坐标,相对于屏幕左上角 */ protected void computeCenter(){ if(center == null){ int x = (int)OVAL.left + (int)((OVAL.right-OVAL.left)/2f); int y = (int)OVAL.top + (int)((OVAL.bottom - OVAL.top)/2f)+50; //状态栏的高度是50 center = new Point(x,y); //Log.v(TAG, "中心坐标:"+center.toString()); } } /** * 获取半径 */ protected int getRadius(){ int radius = (int)((OVAL.right-OVAL.left)/2f); //Log.v(TAG, "半径:"+radius); return radius; } /** * 获取事件坐标相对于饼图的中心x轴正方向的角度 * 这里就是坐标系的转换,本例中使用饼图的中心作为坐标中心,就是我们从初中到大学一直使用的"正常"坐标系。 * 但是涉及到圆的转动,本例中一律相对于x正方向顺时针来计算某个事件在坐标系中的位置 * @param eventPoint * @param center * @return */ protected int getEventAngle(Point eventPoint, Point center){ int x = eventPoint.x - center.x;//x轴方向的偏移量 int y = eventPoint.y - center.y; //y轴方向的偏移量 //Log.v(TAG, "直角三角形两直边长度:"+x+","+y); double z = Math.hypot(Math.abs(x), Math.abs(y)); //求直角三角形斜边的长度 //Log.v(TAG, "斜边长度:"+z); eventRadius = (int)z; double sinA = (double)Math.abs(y)/z; //Log.v(TAG, "sinA="+sinA); double asin = Math.asin(sinA); //求反正玄,得到当前点和x轴的角度,是最小的那个 //Log.v(TAG, "当前相对偏移角度的反正弦:"+asin); int degree = (int)(asin/3.14f*180f); //Log.v(TAG, "当前相对偏移角度:"+angle); //下面就需要根据x,y的正负,来判断当前点和x轴的正方向的夹角 int realDegree = 0; if(x<=0 && y<=0){ //左上方,返回180+angle realDegree = 180+degree; }else if(x>=0 && y<=0){ //右上方,返回360-angle realDegree = 360-degree; }else if(x<=0 && y>=0){ //左下方,返回180-angle realDegree = 180-degree; }else{ //右下方,直接返回 realDegree = degree; } //Log.v(TAG, "当前事件相对于中心坐标x轴正方形的顺时针偏移角度为:"+realAngle); return realDegree; } /** * 获取当前下方箭头位置相对于startDegree的角度值 * 注意,下方箭头相对于x轴正方向是90度 * @return */ protected int getTargetDegree(){ int targetDegree = -1; int tmpStart = startDegree; /** * 如果当前startAngle为负数,则直接+360,转换为正值 */ if(tmpStart < 0){ tmpStart += 360; } if(tmpStart < 90){ /** * 如果startAngle小于90度(可能为负数) */ targetDegree = 90 - tmpStart; }else{ /** * 如果startAngle大于90,由于在每次计算startAngle的时候,限定了其最大为360度,所以 * 直接可以按照如下公式计算 */ targetDegree = 360 + 90 - tmpStart; } //Log.e(TAG, "Taget Angle:"+targetDegree+"startAngle:"+startAngle); return targetDegree; } /** *判断角度为degree坐落在饼图的哪个部分 *注意,这里的角度一定是正值,而且不是相对于x轴正方向,而是相对于startAngle *返回当前部分的索引 * @param degree * @return */ protected int getEventPart(int degree){ int currentSum = 0; for(int i=0; i = degree){ return i; } } return -1; } /** * 在已经得知了当前degree位于targetIndex区域的情况下,计算angle相对于区域targetIndex起始位置的偏移量 * @param degree * @param targetIndex * @return */ protected int getOffsetOfPartStart(int degree, int targetIndex){ int currentSum = 0; for(int i=0; i 0;未超过一半,则offset<0 return offset; } }
程序中用到的Point对象:
public class Point { public int x; public int y; public Point(int x, int y){ this.x = x; this.y = y; } public int[] getPoint(){ int[] point = new int[2]; point[0] = x; point[1] = y; return point; } public String toString(){ return new StringBuilder("[").append(x).append(",").append(y).append("]").toString(); } }
收藏的用户(0) X
正在加载信息~
推荐阅读
最新回复 (2)
- demonsnake 2015-3-5引用 2楼求完整源码下载
- CelinaPritt99 2015-3-5引用 3楼这个项目也并非本人写的,这个还真没源码。 可以参照前面的章节自己实现一个。
站点信息
- 文章2300
- 用户1336
- 访客10861715
每日一句
True success inspires others to act.
真正的成功是激励他人行动。
真正的成功是激励他人行动。
语法错误: 意外的令牌“标识符”
全面理解Gradle - 定义Task
Motrix全能下载工具 (支持 BT / 磁力链 / 百度网盘)
谷歌Pixel正在开始起飞?
获取ElementUI Table排序后的数据
Run-Time Check Failure #0 - The value of ESP was not properly saved across a function call. This is
亲测!虚拟机VirtualBox安装MAC OS 10.12图文教程
华为手机app闪退重启界面清空log日志问题
android ndk开发之asm/page.h: not found
手机屏幕碎了怎么备份操作?
免ROOT实现模拟点击任意位置
新手必看修改DSDT教程
thinkpad t470p装黑苹果系统10.13.2
新会员