前言:首先看看我们的两个demo效果,一个类似支付宝网格属性图,一个类似建行圆形菜单。

这两个效果,第一个涉及自定义view,第二个涉及ViewGroup。如果对于自定义view有一点了解实现起来都不难,但是很多时候自己对于自定义view是一种恐惧,因为写的很少。比如今天的圆形布局的view,其实它并没有想象的那么难,就是三角函数的应用,而且根本不需要记忆,只需要我们知道三角函数的函数图象长什么样子就可以了。

今天说的一分钟掌握圆形布局的原理,肯定一分钟能掌握

现在分析我们的效果一

都知道我们的坐标轴起始点在左上角,现在这个view中的1、2、3、4、5个点的坐标确实不好计算,但是我们把坐标原点移动到view的中心,那么这个正五边形就可以看成一个圆的内切正五边形

现在简单了,夹角可以轻松的算出来,再套用三角函数坐标就得到了各个点的坐标了。然后就是自定义view的知识了。

第一个点的坐标,从x轴正方向开始,根据sin和cos规律,x轴 就是cos,过了90就是负数,y轴 sin过了180就是负数,刚好和安卓的坐标系符合规律,就可以得出坐标。

好吧,闲话扯完,现在我们来一步一步的实现。

1、首先定义一下几个属性

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="MCircle"><attr name="FirstR" format="dimension"/><!-- 第一个圈的半径--><attr name="textSize" format="dimension"/><attr name="textColor" format="color"/><attr name="lineColor" format="color"/><attr name="rectColor" format="color"/><!-- 多边形属性值的颜色-><attr name="unitR" format="dimension"/><!--每个属性的长度--><attr name="attrs" format="string"/><!--属性的名称,用","进行分开--><attr name="datas" format="string"/><!--属性的值是多少,数字用","隔开--></declare-styleable>
</resources><!-- 多边形属性值的颜色-><attr name="unitR" format="dimension"/><!--每个属性的长度--><attr name="attrs" format="string"/><!--属性的名称,用","进行分开--><attr name="datas" format="string"/><!--属性的值是多少,数字用","隔开--></declare-styleable>
</resources>

2、初始化我们的属性

TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MCircle, defStyleAttr, 0);
for (int i = 0; i < ta.getIndexCount(); i++) {int attr = ta.getIndex(i);
    if (attr == R.styleable.MCircle_FirstR) {firstRadius = ta.getDimensionPixelSize(attr, DensityUtil.dip2px(context, 20));
    } else if (attr == R.styleable.MCircle_unitR) {defaultUnit = ta.getDimensionPixelSize(attr, DensityUtil.dip2px(context, 20));

    } else if (attr == R.styleable.MCircle_textSize) {textSize = ta.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));
    } else if (attr == R.styleable.MCircle_textColor) {textColor = ta.getColor(attr, Color.BLACK);
    } else if (attr == R.styleable.MCircle_lineColor) {lineColor = ta.getColor(attr, Color.BLACK);
    } else if (attr == R.styleable.MCircle_rectColor) {rectColor = ta.getColor(attr, Color.BLACK);
    }else if (attr == R.styleable.MCircle_attrs) {String ar = ta.getString(attr);
        if(TextUtils.isEmpty(ar)){mIndexStr =new String[] {"五杀能力", "中单能力", "打野能力", "协作能力", "带崩能力"};
        }}else if(attr==R.styleable.MCircle_datas){String dr = ta.getString(attr);
        if(TextUtils.isEmpty(dr)){initValue =new int[] {2, 0, 3, 1, 0};
        }else{String[] dar = dr.split(",");
            initValue = new int[dar.length];
            for(int index=0;index<dar.length;index++){initValue[index] =Integer.parseInt(dar[index]);
            }}}
}
ta.recycle();

3、绘制

@Override
    protected void onDraw(Canvas canvas) {
        //将画布坐标系移动到view的中心
        canvas.translate(mWidth / 2, mHeight / 2);
        drawRect(canvas);
    }

/*
        绘制多边形
         */
    private void drawRect(Canvas canvas) {
        Path path_rect = new Path();//绘制多边形的路径
        Path path_line = new Path();//绘制圆心与顶点的连线
        Path path_sloid = new Path();//绘制属性值的路径
        for (int i = 0; i < mIndexStr.length; i++) {
            int radus = firstRadius + i * defaultUnit;//每一个多边形的外切圆的半径

for (int j = 0; j < mIndexStr.length; j++) {
                int angle = j * 360 / mIndexStr.length ;//我们的原则是第一个点在x轴正半轴
                                                        // 每一个点对应的角度
                if(initValue.length%2!=0){
                    angle += 360/initValue.length- 88;//如果是边数是奇数的情况,本来是-90,88是我调整了一下
                                                      
                }                                   //如果是偶数边,就没有必要进行偏移,
                                                    // 因为我们的原则是第一个点在x轴正半轴,这个时候多边形是正的
                double radain = Math.PI * angle / 180;
                float x = (float) (Math.cos(radain) * radus);
                float y = (float) (Math.sin(radain) * radus);
                if (j == 0) {
                    path_rect.moveTo(x, y);
                } else {
                    path_rect.lineTo(x, y);
                }
                if (i == mIndexStr.length - 1) { //最后一圈的时候绘制属性
                    //最后一个多边形,画上中心与顶点的连线
                    path_line.lineTo(x, y);
                    canvas.drawPath(path_line, rectPain);
                    path_line.reset();

//绘制文字
                    Rect rect = new Rect();
                    textPain.getTextBounds(mIndexStr[j], 0, mIndexStr[j].length(), rect);
                    if (x < 0) {
                        x = x - rect.width() - 20;
                    } else if (x == 0) {
                        x = x - rect.width() / 2;
                    } else {
                        x += 20;
                    }
                    canvas.drawText(mIndexStr[j], x, y, textPain);
                    //
                    int radus2 = firstRadius + initValue[j] * defaultUnit;
                    float x2 = (float) (Math.cos(radain) * radus2);
                    float y2 = (float) (Math.sin(radain) * radus2);
                    if (j == 0) {
                        path_sloid.moveTo(x2, y2);
                    } else {
                        path_sloid.lineTo(x2, y2);
                    }

}
            }
            path_rect.close();
            canvas.drawPath(path_rect, rectPain);
            path_rect.reset();

}
        path_sloid.close();
        canvas.drawPath(path_sloid, solidPain);
    }


第一个效果介绍完了,那么来看第二个效果,第二个效果遇到了好几个坑,终于还是被我填了。。。

1、圆形控件的坐标位置我们都会算了,那么跟随手指转动,就是计算两个点移动的角度问题,也就是第一个点和第二个点分别于圆形夹角的差。

2、fling效果,刚开始我用的方式是通过fling之后x,y坐标来计算夹角,但是发现有问题,如果是水平方向的fling那么角度就是0,fling就没有效果,于是改良了一下,计算x、和y每次变化的差值,直接当做角度,但是发现转动的非常快,然后我把每次的差值除以10,滑动相对来说可以看得过去了。

3、在计算反正弦的时候,如果x=π/2 ,那么值会无限大,于是会偶尔会出现值=NAN的bug,这就需要在坐标轴上面的点的时候就行判断,在坐标轴上就不要比如0,90,180,270,就不要用反正弦函数了。

一、自定义ViewGroup继承FrameLaout,重写onLayout,把子view放置在圆形上面

int paddingLeft =    getPaddingLeft();int paddingRight = getPaddingRight();int paddiingTop = getPaddingTop();int paddingBottom = getPaddingBottom();width = getMeasuredWidth();height = getMeasuredHeight();int childCount = getChildCount();double angle = 360/childCount*Math.PI/180;int x = 0,y=0;int maxWidth = 0;int maxHeight = 0;for(int i=0;i<getChildCount();i++){View child =  getChildAt(i);int tw = child.getMeasuredWidth();maxWidth = maxWidth>tw?maxWidth:tw;int th = child.getMeasuredHeight();maxHeight = maxHeight>th?maxHeight:th;}int r = Math.min(width-paddingLeft-paddingRight,height-paddiingTop-paddingBottom)/2-Math.max(maxWidth/2,maxHeight/2);for(int i=0;i<getChildCount();i++){View child =  getChildAt(i);x = (int) (Math.cos(angle*i+cPianyi)*r)+width/2- child.getMeasuredWidth()/2;y = (int) (Math.sin(angle*i+cPianyi)*r)+height/2-child.getMeasuredHeight()/2;child.layout(x,y,x+ child.getMeasuredWidth(),y+child.getMeasuredHeight());}

二、写个方法,计算每个点对于圆心点的角度

  public double getAngle(float x, float y){if(y==0&&x>=0){return 0;}else if(x==0&&y>=0){return 90;}else if(y==0&&x<0){return 180;}else if(x==0&&y<0){return 270;}double sA =Math.asin(Math.abs(y)/Math.sqrt(x*x+y*y)) ;if(x>=0&&y>=0){return sA;}else if(x<=0&&y>=0){return Math.PI-sA;}else if(x<=0&&y<=0){return Math.PI+sA;}else if(x>=0&&y<=0){return Math.PI+Math.PI/2+Math.asin(Math.abs(x)/Math.sqrt(x*x+y*y));}return 0;}

三、在dispatchTouchEvent中对move事件进行处理,不修改原来事件分发的逻辑,这样就不影响子view的点击事件了。

 public boolean dispatchTouchEvent(MotionEvent event) {acquireVelocityTracker(event);final VelocityTracker verTracker = mVelocityTracker;switch (event.getAction()){case MotionEvent.ACTION_DOWN:sX =  event.getX()-width/2;sY =  event.getY()-height/2;sa =   getAngle(sX,sY);mPointerId = event.getPointerId(0);if(null!=valueAnimator){valueAnimator.cancel();}break;case MotionEvent.ACTION_MOVE:float cX =  event.getX()-width/2;float cY =  event.getY()-height/2;ca =  getAngle(cX,cY);da = ca-sa;if(da<-Math.PI){da =Math.abs( 2*Math.PI+da);}else if(da>Math.PI){da =-Math.abs(  2*Math.PI-da);}cPianyi=cPianyi+da;Log.i("aaa","cPianyi:"+da+",ca:"+ca+",sa:"+sa);fixPianyi();sa = ca;requestLayout();break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:verTracker.computeCurrentVelocity(1000, mMaxVelocity);velocityX = verTracker.getXVelocity(mPointerId);velocityY = verTracker.getYVelocity(mPointerId);velocityX = Math.max(Math.abs(velocityX),Math.abs(velocityY));if(velocityX>1000){flingSX=event.getX();flingSy=event.getY();valueAnimator = new ValueAnimator();valueAnimator.setDuration(2000);valueAnimator.setInterpolator(new DecelerateInterpolator());valueAnimator.setFloatValues(0,1.0f);valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {public float px;@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float fraction =   animation.getAnimatedFraction();float cx = flingSX+velocityX*fraction;double flingangle =Math.abs (cx-px)*(Math.PI/180);px = cx;if(da>0){flingangle = -flingangle;}cPianyi=cPianyi-flingangle/10;fixPianyi();requestLayout();}});valueAnimator.start();}releaseVelocityTracker();break;}return super.dispatchTouchEvent(event);}

四、这个时候你会发现之后按住子视图的button才可以转动,那是因为我们没有消费down事件,所以加上

public boolean onTouchEvent(MotionEvent event) {switch (event.getAction()){case MotionEvent.ACTION_DOWN:return true;}return super.onTouchEvent(event);}
五、VelocityTracker 和 属性动画就没得讲了,必备基础知识而已。。。

最后,如果是想学习怎么写,一定自己把第一个demo自己写一遍,自己以后就再也不怕圆形布局了,至于第二个demo也就的上面讲的了。同样的原理,每次转动的时候吧偏移的角度加在原来的基础上就可以了。

源码下载

android 一分钟掌握圆形布局原理--圆形菜单控件 so easy相关推荐

  1. Kotlin 开发Android app(十二):Android布局FrameLayout和ViewPager2控件实现滚动广告栏

    在上一节中我们简单的介绍了RecyclerView 的使用,他是整个开发的重点控件,这一节我们来看看FrameLayout 布局结合ViewPager2,开发一个广告控件. 新模块banner 先创建 ...

  2. Android 软键盘弹出时把布局顶上去,控件乱套解决方法

    Android 软键盘弹出时把布局顶上去,控件乱套解决方法 参考文章: (1)Android 软键盘弹出时把布局顶上去,控件乱套解决方法 (2)https://www.cnblogs.com/zhuj ...

  3. android怎么查看方法被谁调用,Android中查看布局文件中的控件(view,id)在哪里被调用(使用)...

    在阅读别人的代码时通常是很痛苦的,有时很想要看一看布局中的控件在哪里被调用了,为之很苦恼 在这里提供一种方法. 复制要查看的控件ID,到R文件中搜索到该ID, 接下来就好办的了,选中ID按下Ctrl鼠 ...

  4. android 仿ios三级联动,仿iOS的PickerView控件,有时间选择和选项选择并支持一二三级联动效果...

    Android-PickerView 注意事项.详请使用方式.更新日志等,请查看 Wiki文档 Wiki文档,Wiki文档,Wiki文档 !~ 重要的事情说三遍 对于使用上有任何疑问或优化建议等,欢迎 ...

  5. Android之RemoteViews篇上————通知栏和桌面小控件

    Android之RemoteViews篇上----通知栏和桌面小控件 一.目录 文章目录 Android之RemoteViews篇上----通知栏和桌面小控件 一.目录 二.RemoteViews的概 ...

  6. Android获取景点的信息,景点介绍(ListView控件应用)

    1. 案例概述 此案例主要是对ListView列表控件的使用.当应用中包含多项数据,每项数据结构相同,只是内容不同时,可通过列表显示.对于列表中的内容,可以是显示字符串的TextView,也可以是结构 ...

  7. 《深入理解Android 卷III》第六章 深入理解控件(ViewRoot)系统

    <深入理解Android 卷III>即将发布,作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分. ...

  8. Android ActionBar高级自定义——设置标题居中和添加控件

    转载地址:http://blog.csdn.net/gtbluesky/article/details/44656567 关于ActionBar的一些常见使用方法我已经在之前两篇博文(Android ...

  9. android设置屏幕高度和宽度设置,Android手机的屏幕宽高度和代码设置控件的宽高度...

    1.Android手机的屏幕宽高度 WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); int w ...

最新文章

  1. [转] 如何从多份Java/JEE工作中进行抉择
  2. ubuntu14.04交叉编译vlc2.1.5源码,编译出在win32下运行的程序
  3. 百度前端fex-team团队面试指南
  4. GPU 机器学习开箱即用
  5. ES6,Array.copyWithin()函数的用法
  6. Flutter AnimatedSwitcher 实现优美的图片切换动画
  7. C++_类和对象_C++继承_继承中的对象模型_占内存大小---C++语言工作笔记063
  8. [GCN] 图卷积知识梳理 -持续更新
  9. PHP推流地址获取图片
  10. 使用MicroPython开发ESP32(06):WebServer功能实现简单说明
  11. CSDN内容变现渠道
  12. 应用统计学与计算机论文,浅谈统计学在生活中的应用
  13. html5实现在线动态画板,HTML5 canvas实现一个简易画板
  14. 微信英文名是WeChat,那么你知道微信小程序的英文名吗?
  15. yate--sip server的学习过程
  16. Java---(SpringBlade框架)后台从数据库读取所有点的经度和纬度,传输到前端显示在地图上
  17. 【镜像取证篇】VMware虚拟机配置文件取证
  18. 宝塔自动备份网站到FTP空间
  19. 关闭 C4996 警告(_CRT_SECURE_NO_DEPRECATE)方法
  20. 批量下载人像图片的技巧,POCO相册图片如何下载的方法

热门文章

  1. 初识java——Java语言简介
  2. 随笔分类 - 零基础学习iOS开发
  3. 小梅哥——38译码器
  4. excel批量分割为印刷版pdf:模板的自动生成
  5. 进程管理:ps top nice
  6. 互联网+废品回收项目数据库设计说明书
  7. 高数习题第八章总练习题(下)
  8. 旋转机械 | 基于ANSYS WB平台的滑动轴承分析工具(一)
  9. PostgreSQL 客户端下查看表,视图,function,切换数据库等
  10. 梯度爆炸和梯度消失的本质原因