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

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

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

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

现在分析我们的效果一

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

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

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

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>

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)
                        @Override
                        public 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也就的上面讲的了。同样的原理,每次转动的时候吧偏移的角度加在原来的基础上就可以了。

源码下载

时间: 2024-08-29 10:31:44

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

android 在布局中动态添加控件

第一步 Java代码 final LayoutInflater inflater = LayoutInflater.from(this); 第二步:获取需要被添加控件的布局 Java代码 final LinearLayout lin = (LinearLayout) findViewById(R.id.LinearLayout01); 第三步:获取需要添加的布局(控件) Java代码 LinearLayout layout = (LinearLayout) inflater.inflate( R

Android 布局之LinearLayout 子控件weight权重的作用详析

关于Android开发中的LinearLayout子控件权重android:layout_weigh参数的作用,网上关于其用法有两种截然相反说法: 说法一:值越大,重要性越高,所占用的空间越大: 说法二:值越大,重要性越低,所占用的空间越小. 到底哪个正确?哪个错误?抑或还有其他解释?请点击查看关于weight 权重参数作用的详分析: 其实这两种情况都不太准确: 准确的解释是,weight 权限 是用于分配父控件某一方向上尺寸-所有子控件在该方向上设定尺寸和 所得值的一个参数,把这个相减得到的结

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

<深入理解Android 卷III>即将公布,作者是张大伟.此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分. 在一个特别讲究颜值的时代,本书分析了Android 4.2中WindowManagerService.ViewRoot.Input系统.StatusBar.Wallpaper等重要"颜值绘制/处理"模块 第6章 深入理解控件(ViewRoot)系统(节选) 本章主要内容: ·  介绍创建

自个儿写Android的下拉刷新/上拉加载控件 (续)

本文算是对之前的一篇博文<自个儿写Android的下拉刷新/上拉加载控件>的续章,如果有兴趣了解更多的朋友可以先看一看之前的这篇博客. 事实上之所以会有之前的那篇博文的出现,是起因于前段时间自己在写一个练手的App时很快就遇到这种需求.其实我们可以发现类似这样下拉刷新.上拉加载的功能正在变得越来越普遍,可以说如今基本上绝大多数的应用里面都会使用到.当然,随着Android的发展,已经有不少现成的可以实现这种需求的"轮子"供我们使用了. 但转过头想一下想,既然本来就是自己练手

Android实战(一)学习了多个控件实现登录及记住密码功能

首先确定一下需要的控件: 两个EditText:用于输入账号和密码 一个button:用于登录查看账号和密码是否正确 一个checkbox:用于记住密码和账户 一个Androidstudio:用于编写代码,当然牛逼的人也推荐使用记事本写代码,废话不多说开工. 创建一个App项目加入两个布局两份Java.class ,在Androidmanifest.xml里面注册第二个布局. 准备完毕 1.在初始布局中加入上述控件,并为其设置好id 代码如下所示 <LinearLayout xmlns:andr

javascript 框架、根基技巧、布局、CSS、控件 JavaScript 类库

预筹备之 JavaScript 今朝支流的 JavaScript 框架排名中,jQuery 和 Ext 可算是佼佼者,得到了用户的普遍好评.海内的一些框架许多也是模仿 jQuery 对 JavaScript 停止了包装,不外这些框架的开山祖师 YUI 照样坚持用自己的 JavaScript 类库. jQuery 是今朝用的最多的前端 JavaScript 类库,据初步统计,今朝 jQuery 的占有率曾经跨越 46%,它算是比拟轻量级的类库,对 DOM 的操纵也比拟便利到位,支撑的后果和控件也许

android自定义View之(七)------自定义控件组合仿actionbar控件

我们前面写了6个自定义view的样例,这都是全新自已画的控件.在这个样例中,我们来用几个现有的控件来组合成一个新的控件. 效果图: 我们用二个Button和一个TextView组合来成为一个actionbar,下面先来一个效果图: 关键代码: (1)res/layout/custom_action_bar.xml----组合控件布局文件 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android&quo

Android02.常用布局及基本UI控件

一.Android学习API指南:[了解] 1. 应用的组成部分   App Components 1.1. 应用的基本原理    App Fundamentals 1.2. Activity      Activities 1.2.1. 片段    Fragments 1.2.2. 加载器     Loaders 1.2.3. 任务和返回堆    Tasks and Back Stack 1.3. Service服务   Services 1.3.1. 绑定服务     Bound Servi

Android 自己定义圆圈进度并显示百分比例控件(纯代码实现)

首先,感谢公司能给我闲暇的时间,来稳固我的技术,让我不断的去探索研究,在此不胜感激. 先不说实现功能,上图看看效果 这个是续上一次水平变色进度条的有一个全新的控件,理论实现原理 1.分析控件:该控件基本上是圆圈内嵌圆圈: 2.进度计算:事实上是小学二年级数学题:当前进度/总数=百分比: 3.中间时间:呵呵,纯粹忽悠,不解释(当前时间). 理论总是和实践差距的太远.不扯淡.不吹嘘,贴代码: package com.spring.progressview; import java.text.Simp