人生难得几回搏,此时不搏待何时。这篇文章主要讲述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();
【android 一分钟掌握圆形布局原理--圆形菜单控件 so easy】
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也就的上面讲的了。同样的原理, 每次转动的时候吧偏移的角度加在原来的基础上就可以了。
源码下载
推荐阅读
- Android开发--adb,SQLite数据库运用
- 信息安全需求简要介绍
- CommVault Systems 2020面试经验分享(校园)
- Traveloka SDE3面试体验详细分享(校园)
- 算法题(如何打印给定字符串的所有子字符串())
- C++如何使用指针与引用(它们有什么区别?)
- Java如何使用方法(用法解释和代码示例)
- 高盛面试经验|S22(校园内面试分析概要文件)
- PHP如何使用readdir()函数(代码用法示例)