上次实现了圆形菜单的绘制【http://www.cnblogs.com/webor2006/p/7525979.html】,这次对它加上触摸旋转效果,实际上就是建行手银APP里面的一个效果,先贴上最终效果:
整理思路:
那如何才能够实现上面这种效果呢?先来分析下上一次的代码:
public class CircleMenu extends ViewGroup { /* 代表是子视图所在圆的直径,由 */ private int d = 480; private int startAngle; public CircleMenu(Context context) { this(context, null); } public CircleMenu(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CircleMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { } public void setData(int[] resIds, String[] texts) { for (int i = 0; i < resIds.length; i++) { View view = View.inflate(getContext(), R.layout.circle_item, null); ImageView image_icon = (ImageView) view.findViewById(R.id.image_icon); image_icon.setImageResource(resIds[i]); TextView tv_label = (TextView) view.findViewById(R.id.tv_label); tv_label.setText(texts[i]); addView(view); } } //处理控件及其子控件的测量 //在自定义ViewGroup中,系统默认只处理当前控件的测量,不对子视图进行测量,需要重写onMeasure对其子视图进行测量 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //处理用户不管宽高传多大值都要正常能显示的问题 // super.onMeasure(widthMeasureSpec, heightMeasureSpec);//默认的处理已经满足不了我们的需求了 int measuredWidth; int measuredHeight; //判断模式:主要是处理两种:1、确切的;2、至多 int mode = MeasureSpec.getMode(widthMeasureSpec); int size = MeasureSpec.getSize(widthMeasureSpec); if (mode != MeasureSpec.EXACTLY) { //则宽应该是WRAP_CONTENT或未指定 // FAQ:1、如果用户没指定大小,那如何显示呢?这里需求是:背景有多大控件就有多大;2、如果木有背景,则用默认宽度作为控件宽度 int suggestedMinimumWidth = getSuggestedMinimumWidth();//获取背景的宽度 if (suggestedMinimumWidth == 0) { //无背景 measuredWidth = measuredHeight = getDefaultWidth(); } else { //有背景 measuredWidth = measuredHeight = Math.min(suggestedMinimumWidth, getDefaultWidth()); } } else { //说明用户指定了确切的宽高,则判断传入的宽度和屏幕宽度取较小值 measuredWidth = measuredHeight = Math.min(size, getDefaultWidth()); } d = measuredWidth;//当测量值确定之后,则将直径也动态改变一下既可 Log.d("cexo", "measuredWidth:" + measuredWidth + ";measuredHeight:" + measuredHeight); setMeasuredDimension(measuredWidth, measuredHeight); for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); //默认系统是可以通过onMeasure给予MeasureSpec参数的,而对于inflate进来的子视图是没有MeasureSpec参数的 //而子视图都是通过inflate过来的,所以:需要我们自己设计MeasureSpec int makeMeasureSpec = MeasureSpec.makeMeasureSpec(/*200*/d / 3, MeasureSpec.EXACTLY); child.measure(makeMeasureSpec, makeMeasureSpec); } } //获取屏幕宽高的最小值 private int getDefaultWidth() { //获取屏幕宽高 DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); int screenWidth = displayMetrics.widthPixels; int screenHeight = displayMetrics.heightPixels; //获取宽高的较小值 int result = Math.min(screenWidth, screenHeight); return result; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); //temp:相当于自定义控件所在圆的圆心到子视图所在矩形的几何中心的距离 float temp = d / 3.0f; int childWidth = child.getMeasuredWidth(); int left = (int) (d / 2 + Math.round(temp * Math.cos(Math.toRadians(startAngle))) - childWidth / 2); int right = left + childWidth; int top = (int) (d / 2 + Math.round(temp * Math.sin(Math.toRadians(startAngle))) - childWidth / 2); int bottom = top + childWidth; child.layout(left, top, right, bottom); startAngle += 360 / getChildCount();//每次角度进行累加 } } }
其中标红了一个字段"startAngle":绘制圆形菜单的起始角度,默认是从0度开始绘制的:
那看好了,接下来我们将startAngle的初始角度改变一下:
这时看此时的效果:
于是乎实现思路产生了:就是通过用户的触摸点来得到角度相对于当前角度的差值,然后再将startAngle去加上这个差值,最终再通知整体布局重绘则就能实现如上的效果,接下来就按这种思路去一一实现!
具体实现:
很显然需要给当前的ViewGroup处理onTouchEvent事件,所以:
下面运行看下打印情况:
呃,为啥只产生了down事件呢,明明move了呀,这是为啥呢,直接给出处理代码:
再次看输出:
完美解决,至于这样写的背后的原因这里先不用管,因为涉及到之后要学习的事件传递机制,之后会系统全面的从源码角度彻底来理解android的事件传递机制,到那时对于这一块的代码就能完全理解啦~
【注】:由于我GIF截屏软件的原因,效果上跟实际显示的有些差异,将就着看,主要是能表达清楚意思既可~~
接着就可以写我们的触摸逻辑了,首先先记录down时的x,y坐标:
接下来就是怎么通过触摸点的坐标跟圆的半径来计算出角度了,这里先以计算当前点击的坐标的角度为例:
如果对于为啥要对坐标进行转换不太清楚的,可以参考这篇博客,里面对其原理有详细的说明:【http://www.cnblogs.com/webor2006/p/7687320.html】,有了坐标点之后,如何来计算出它的角度呢,下面来复习一下数学公式:
∵ 角度=180/π * 弧度
∴ 这时只要求得弧度,那角度也就迎刃而解了,如何求弧度呢?
这里可以采用Math.asin()反正弦函数来获得:
所以接下来得求角度的sin,而它的求解可以先看下图:
而目前斜边z是未知的,而对于已知真角边(x,y)求直角函数的斜边可以通过Math.hypot(x,y)获取:z = Math.hypot(x,y)
所以其角度公式为:(弧度 * 180 / Math.PI) = (Math.asin(角度的正弦值) * 180 / Math.PI) = (Math.asin(y / z) * 180 / Math.PI) = (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI)
所以可以通过这个公式来获取角度值啦:
然后根据象限的不同,返回的角度有正也有负,下面运行:
按照右上为第一象项,右下为第四象限,可以看到值有正也有负,这里在之后算两个角度的差是需要根据象限来做特珠处理的,所以这里得想办法获取当前点击的坐标属于哪个象限,比较简单,直接上代码:
运行看输出:
像目前获得角度和象限可以将其代码封装成通用的工具方法,便于其它有类似的需求去用,另外代码也显得比较整洁,所以修改代码如下:
对于需要计算的值都已经知道怎么算了,那接下来就要真正来处理自己的逻辑了,这时得把我们测试计算的代码挪到move上来,具体如下:
但是!!这时候代码还有缺陷,此时还不能做到让子布局去重新布局,我们知道让View重新执行onDraw()重绘可以用invalidate()方法,但是!!!这是ViewGroup,我们应该是让它重新执行它的onLayout方法重新去布局并绘制,也就是执行它:
这里需要使用另外一个API,如下:
看下效果:
呃~~感觉有点问题,并没有随着手指移动多少而移动,这是为啥呢?因为还差一个逻辑细节问题需要处理,下面来看下代码:
所以加上代码:
至此就完美实现效果啦!!