您的位置:首页 > 移动开发 > Android开发

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

2016-11-19 11:49 525 查看
前言:首先看看我们的两个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也就的上面讲的了。同样的原理,每次转动的时候吧偏移的角度加在原来的基础上就可以了。

源码下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息