Android 自定义控件实现弹性旋转的圆形菜单
来源:互联网 发布:淘宝唱片店正版 编辑:程序博客网 时间:2024/05/22 17:14
自定义控件实现弹性旋转的圆形菜单
- 写这个之前参考了一下其他类似的控件,自己实现了一下并做如下记录
- 使用到的包含“事件拦截”、“三角函数”、“View测量”、“View布局”
- 控件的效果图如下,旋转动画、弹性旋转、item点击
流程梳理
- 首先要实现一个圆形的菜单控件我们选择继承ViewGroup;
- 第一步考虑在onMeasure中对所有Child进行测量,测量完成后onLayout才可以获取到Child的测量宽高;
- 第二步考虑Child排版问题,也就是核心代码中onLayout的过程;
- 在这个过程中,定义了“当前旋转角度”、“旋转中心”、旋转半径;
- 获取容器内部Child个数,计算相邻Child角度间距;
- 在已知旋转中心、半径、当前旋转角度、相邻Child角度间距后就可以为Child排版啦;
- 这里根据角度及其所在象限,计算出该角度相对于每个象限的角度并计算其正切值;
- 在已知半径、正切值就可以通过三角函数tan(A)=a边/b边、勾股定理a²+b²=c²计算出a边长、b边长;
- 根据Child所在的象限,我们可以利用a边、b边计算出该Child中心点所在的坐标;
- 知道这样就可以对该Child进行布局了,即调用Child.onLayout(l,t,r,b),布局时需要使用Child的测量宽高;
- 第三步考虑触摸事件的拦截,这里使用容器onInterceptTouchEvent 方法在滑动距离大于系统touchSlop时进行拦截;
- 一旦ViewGroup决定拦截该事件,那么后续的事件都会调用容器onTouchEvent;
- 我们在onTouchEvent 的Move时,计算前后2次事件的偏转角度来更改“当前选中角度”并要求容器重新布局 requestLayout(); 以此达到旋转的目的;
- 我们在onTouchEvent 的Up时计算本次旋转在1秒内旋转过的角度是否达到弹性旋转
- 如果需要弹性旋转则根据当前的速度,设定Runnable 进行弹性旋转;
代码块
控件的代码如下:
public class RotateView extends ViewGroup { private static final String TAG = "RotateView"; /** * 当前已旋转的角度 ,当改变该角度时,并重新布局则达到旋转的效果 */ private float mCurAngle = 0f; /** * 记录每次旋转开始时的角度 */ private float mStartRotateAngle; /** * 当前ViewGroup 旋转的中心点坐标 */ private PointF mCenterPoint; /** * 围绕中心点旋转的半径 */ private double mR; /** * 缓存每个Child 布局时所在的位置 */ private PointF mChildPoint = new PointF(); /** * 系统可检测的最小滑动距离 */ private int touchSlop; /** * 记录每次MotionEvent 的坐标值 */ private float mLastX; private float mLastY; /** * 记录开始滑动的时间 */ private long mStartRotateTime; /** * 触发弹性旋转的边界值 */ private static final float ROTATE_RATE = 500; /** * 弹性旋转Runnable */ private RotateRunnable action; /** * 当前是否处于弹性旋转状态 */ private boolean isFling; public RotateView(Context context) { this(context, null); } public RotateView(Context context, AttributeSet attrs) { super(context, attrs); mCenterPoint = new PointF(); touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //1、对每个Child进行测量 在测量之后才能获取到Child的 MeasureHeight 和MeasureWidth int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int width = getWidth(); int height = getHeight(); //2、初始化旋转的中心点 mCenterPoint.x = width / 2; mCenterPoint.y = height / 2; //3、初始化旋转的半径 mR = ((Math.min(width, height) / 2d) * 0.6d); Log.i(TAG, "Angle=" + mCurAngle + " Cx=" + mCenterPoint.x + " Cy=" + mCenterPoint.y); if (getChildCount() > 0) { //4、每相邻的Child的角度间距 float mPerAngle = 360f / getChildCount(); //5、根据角度开启每个Child 排版过程 for (int i = 0; i < getChildCount(); i++) { //6、计算当前Child角度 保证其在Child在 360>angle>=0 float angle = (mCurAngle + mPerAngle * i) % 360f; if (angle < 0) { //比如Child当前角度为-30,那么它其实就是 360-30=330度是一样的 angle = 360f - Math.abs(angle) % 360f; } View child = getChildAt(i); //7、计算当前Child位于哪个象限 int quadrant = getQuadrant(angle); //8、计算该角度正切值 可能为0 double tanA = getTanA(quadrant, angle); //9、根据正切值获取A边边长 double edgeA = getEdgeA(tanA);//A边 double edgeB = edgeA == 0 ? mR : edgeA / tanA;//B边 Log.i(TAG, "child=" + i + " 象限=" + quadrant); //10、根据child所在象限 及 A边 B边 计算Child中心点位置坐标 computeChildLocation(quadrant, edgeA, edgeB); //11、根据Child中心点坐标及Child大小 进行布局 layoutChild(child); } } } /** * 根据Child中心点位置进行布局 */ private void layoutChild(View child) { int measuredHeight = child.getMeasuredHeight(); int measuredWidth = child.getMeasuredWidth(); int left = (int) (mChildPoint.x - measuredWidth / 2); int top = (int) (mChildPoint.y - measuredHeight / 2); int right = (int) (mChildPoint.x + measuredWidth / 2); int bottom = (int) (mChildPoint.y + measuredHeight / 2); child.layout(left, top, right, bottom); } /** * 根据child所在象限 及A边 B边 计算其所在位置 */ private void computeChildLocation(int quadrant, double edgeA, double edgeB) { //1象限 tan A= y/x; 2象限 tan A =x/y ;3象限 y/x ;4象限 x/y switch (quadrant) { case 1://第一象限 mChildPoint.x = (float) (mCenterPoint.x + edgeB); mChildPoint.y = (float) (mCenterPoint.y - edgeA); break; case 2://第二象限 mChildPoint.x = (float) (mCenterPoint.x - edgeA); mChildPoint.y = (float) (mCenterPoint.y - edgeB); break; case 3://第三象限 mChildPoint.x = (float) (mCenterPoint.x - edgeB); mChildPoint.y = (float) (mCenterPoint.y + edgeA); break; default://第四象限 mChildPoint.x = (float) (mCenterPoint.x + edgeA); mChildPoint.y = (float) (mCenterPoint.y + edgeB); break; } } /** * 根据正切值获取A边长度 */ private double getEdgeA(double tanA) { if (tanA == 0) {//如果正切值=0 ,那边A边长度=0,B边长=半径 return 0; } else {//否则根据直角三角函数 a²+b²=c² tan(A)= a/b return Math.sqrt((mR * mR * tanA * tanA) / (1 + tanA * tanA)); } } /** * 角度转弧度并获取正切值 * * @param quadrant 象限 * @param angle 角度 * @return 正切值 */ public double getTanA(int quadrant, float angle) { float A; switch (quadrant) { case 1: A = angle; break; case 2: A = angle - 90; break; case 3: A = angle - 180; break; default: A = angle - 270; break; } //角度转弧度 求正切值 return Math.tan(Math.toRadians(A)); } /** * 获取象限 */ public int getQuadrant(float rAngle) { if (rAngle >= 0 && rAngle < 90) { return 1; } if (rAngle >= 90 && rAngle < 180) { return 2; } if (rAngle >= 180 && rAngle < 270) { return 3; } return 4; } public void setMCurAngle(float mCurAngle) { this.mCurAngle = mCurAngle; requestLayout(); } public void startAnim() { ObjectAnimator animator = ObjectAnimator.ofFloat(this, "MCurAngle", mCurAngle, mCurAngle + 720f); animator.setDuration(5 * 1000); animator.start(); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { float x = ev.getX(); float y = ev.getY(); boolean intercepted = false; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: //Down首先判断当前是否处于弹性旋转状态 if (isFling) {//如果是则重置状态,并移除弹性旋转,也就是按下立刻停止旋转 isFling = false; removeCallbacks(action); return true; } break; case MotionEvent.ACTION_MOVE: { //判断滑动的距离 只有大于系统的可识别滑动距离则容器拦截事件 float diffX = Math.abs(x - mLastX); float diffY = Math.abs(y - mLastY); if (diffX >= touchSlop || diffY >= touchSlop) { //记录开始旋转的角度 mStartRotateAngle = mCurAngle; //记录开始旋转的时间 mStartRotateTime = System.currentTimeMillis(); intercepted = true; } } break; case MotionEvent.ACTION_UP: break; } mLastX = x; mLastY = y; return intercepted; } @Override public boolean onTouchEvent(MotionEvent event) { float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_MOVE: { //计算上一次事件所在的角度 float startAngle = getAngle(mLastX, mLastY); //计算本次事件所在的角度 float endAngle = getAngle(x, y); float changeAngle = startAngle - endAngle; //获取当前事件所在的象限 int quadrant = getQuadrant(x, y); float curAngle; if (quadrant == 1 || quadrant == 4) { curAngle = mCurAngle + changeAngle; } else { curAngle = mCurAngle - changeAngle; } //设置当前旋转角度并重新布局 setMCurAngle(curAngle); } break; case MotionEvent.ACTION_UP: { long rotateDuration = System.currentTimeMillis() - mStartRotateTime; float sweepAngle = mCurAngle - mStartRotateAngle; //计算每秒活动的角度 float speed = sweepAngle * 1000 / rotateDuration; Log.i(TAG, "speed=" + speed); if (Math.abs(speed) > ROTATE_RATE) { action = new RotateRunnable(speed); post(action); } } break; } mLastX = x; mLastY = y; return true; } private float getAngle(float xTouch, float yTouch) { double x = xTouch - mCenterPoint.x; double y = yTouch - mCenterPoint.y; return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI); } public int getQuadrant(float x, float y) { if (x >= mCenterPoint.x) { return y >= mCenterPoint.y ? 1 : 4; } else { return y < mCenterPoint.y ? 2 : 3; } } public class RotateRunnable implements Runnable { RotateRunnable(float speed) { this.speed = speed; } float speed; @Override public void run() { if (Math.abs(speed) < 20) { isFling = false; return; } float addAngle = mCurAngle + (speed / 30); setMCurAngle(addAngle); isFling = true; speed /= 1.0666F; postDelayed(this, 30); } }}
- 代码在GitHub上:https://github.com/yushilei1218/MyApp2.git;
Br
0 0
- Android 自定义控件实现弹性旋转的圆形菜单
- Android圆形旋转菜单
- Android 圆形旋转菜单
- android 自定义控件之圆形菜单
- Android自定义View实现不断旋转的圆形图片
- Android自定义圆形菜单
- Android CircleMenu 圆形旋转菜单
- Android 圆形旋转菜单【转】
- 自定义控件:旋转菜单
- Android自定义控件NumberCircleProgressBar(圆形进度条)的实现
- Android自定义控件实现圆形进度CircleProgressBar
- Android自定义ImageView实现圆形控件显示
- Android自定义控件实现圆形进度CircleProgressBar
- Android实现圆形头像-使用自定义控件
- Android 自定义控件实现圆形进度条
- Android自定义控件实现圆形进度条
- 自定义控件-绕着圆形轨迹旋转的小球
- android自定义控件(一):写一个圆形菜单的Layout
- [Polya计数] BZOJ 3202 [Sdoi2013]项链
- 转载:NTFS交换数据流实验(晴刃原创)
- connect成功之后,accept返回之前,客户端和服务器的链接是否已建立
- Servlet Response对象
- ds1302简单解析
- Android 自定义控件实现弹性旋转的圆形菜单
- Servlet 重定向原理
- 数据结构——字符串处理
- Servlet中文乱码问题
- 处理大并发之四 使用libevent利器bufferevent
- linux 的shell 和 makefile 2017-2-22
- SpringMVC @RequestBody接收Json对象字符串
- Android 代码混淆异常 transformClassesAndResourcesWithProguardForRelease FAILED
- Java Cache系列之Guava Cache