Android 圆盘旋转/飞转菜单(高度定制化)
2017-02-13 13:42
253 查看
看了建行的圆盘菜单,效果还不错,于是也动手试试做一个,目标——高度定制化,数量、样式及动画。
为什么要用适配器做成定制化?你知道的,UI那边总是动不动就改的,加点什么啊,删点什么啊,而且有多态机器需要适配,底色不一样就算了,数量和Item也不一样,怎么搞啊?我们总不能每一次都改一大串吧,改一大串和重复类似工作对我们来说简直就是折磨,所以,需要定制化。当然,如果第二次就直接淘汰圆盘了,那另当别论,。
工程代码:https://github.com/aknew123/CircleMenu 点击打开链接
效果如下:
程序架构的UML图,如下:
一、圆盘菜单自定义控件的使用
网上查看了一下,看一下他们的实现方式千篇一律,功能都写在一个文件里,阅读难度稍大,于是采用适配器模式做一个,把view和实现逻辑分离,就是简单的MVC,代码结构如下图:
当然,这只是一个Library,把DefaultMenuAdapter删了,重新编译就可以直接用jar包了,测试模块写在另一个工程,使用示例如下:
后面的是用户定制化动画实现(提供诸多状态,爱怎么折腾怎么折腾)。好了,我们来看看实现原理。
二、实现原理
1.先看需要做什么
(1).圆盘菜单CircleMenu是一个转盘,装有各种Item,是一个容器,那理所当然是继承ViewGroup,为了方便实现飞转,所以监听用户手势OnGestureListener;
(2).菜单项CircleItemView,虽是一个item,但为了可定制化,当然也是一个容器,也是为了方便实现飞转,重写onFling方法,所以这里继承LinearLayout,纯属为了方便布局,若有特殊需求,可在布局的时候,在外层添加一个FrameLayout容器,爱怎么搞怎么搞;
(3).Adapter和Model,既然有Item那肯定也有Adapter和数据model,Adapter继承BaseAdapter即可,就跟ListView一样。
好了,就这3个玩意儿,另外几个都是从上面这两个抽出来的。
2.旋转原理
来看一下转动分析图,
图中圆心的坐标应为( mRadius, mRadius),则圆盘半径为mRadius,按照大多数人的习惯右手向下滑动,圆盘也就跟着顺时针转动(或者说滚动),当然不是圆盘在转,是菜单项在滚动,滚动了弧度为a,那途中Item的x、y坐标应为
x = tmp *cos a;
y = tmp*sin a;
这是相对于圆心的坐标,再加上圆盘的半径,就是圆盘中的坐标了,子View只管在父容器中的坐标,父容器布局时会加上自身的left和top,这样逐层往上推就是在屏幕中的坐标了)。
我们将Item强制为正方形,itemWidth为Item的宽度,当 tmp = mRaiuds - itemWidth / Math.sqrt(2) 时,那Item的外直角就走在圆盘圆周上,再大则要出边界了,所以,Item的中心点位于mRaiuds/2和mRaiuds - itemWidth / Math.sqrt(2)之间最为合理,那么,item的x、y为
left 为 x, top 为y,mStartAngle为间隔角度,一个for循环就可以把所有Item按角度Layout出来。要让他们滚动起来,只要在ACTION_MOVE的时候给一个角度就行。那转动原理就这样了。
3.测量和布局
Android View 的绘制过程,如下图
过程为 measure() ---> layout () ---> draw(),measure() 中会掉用onMeasure()方法, layout () 中调用onLayout()方法,draw()我们就不管了,因为还是SDK继承而来,他可以自己绘制。
onMeasure方法实现如下:
onLayout()在父类里是一个抽象方法,没有返回值,所以其中没有改变任何成员变量的话,是没有效果的。
4.圆盘滚动
圆盘的滚动,可以在onTouchEvent(MotionEvent event)方法里实现,但这意味着你要同时在CircleMenu和CircleItemView的onTouchEvent方法里都要实现,当然两套代码可以提出来(但有可能子View和父View的x、y坐标不同,有麻烦),由于我们知道Android事件分发机制,Touch事件首先是父View的dispatchTouchEvent(MotionEvent
event)先获得,然后往下分发,所以在dispatchTouchEvent方法实现Item转动一举两得,还有个原因是我们还要监听手势,需要重写onTouchEvent方法,so。。。。Item的滚动通过重写dispatchTouchEvent实现,代码如下:
简单吧,呵呵,提出来了,提到了RotateEngine.java里实现,如下:
之所以代码这么多,是因为要处理诸多逻辑和诸多状态,主要思路就是得到x、y差值,计算角度差值,根据象限给加减角度,看ACTION_MOVE这段即可当然里面还牵扯到后面3个方法也差不多看完了。
5.圆盘飞转
飞转显然就是Fling,肯定是要在onFling()方法里面实现的,那么要实现OnGestureListener接口,同时,重写dispatchTouchEvent(MotionEvent event)方法,代码如下:
再来看onFling方法
也很简单,都提出来的,看FlingEngine.java。
之所以要提出startFling方法是因为Item也要用到,通过getParent强转之后调用,所以,CircleItemView.java的代码就相当简单了,如下:
CircleMenu.java的代码就不贴出来了,上https://github.com/aknew123/CircleMenu上downloade吧,里面有状态控制,其实代码也不到400行,只是贴进来显得文长(想想还是贴吧)。
下面说说适配器模式,按开发的角度,应该一开始就该说了,要不怎么调试啊,好!马上来!
6.适配器模式
想要定制化就要用适配器模式,这样才人性化,要不只能写一些只能用一次的逻辑代码而已。适配器模式也是一个小的MVC模式,
Modle ----- DefaultItem
View ---- CircleItemView
Controller ---- Adapter
用起来,其实挺简单,当然这是简单的MVC,父容器CircleMenu要实现2个方法,如下:
其实就是装饰(装饰模式)了一下Adapter,然后调用Adapter的getView方法,即
mAdapter.getView(i, null, this);
然后,Adapter就更简单了,跟ListView一样,继承BaseAdapter,再重写这4个方法,DefaultMenuAdapter.java代码如下
这样就做到,高度定制化。
7.状态模式定制动画
可根据圆盘状态,定制与之相匹配的动画,想要多炫有多炫,只要监听setOnStatusChangedListener方法即可实现。
是不是很炫啊
完!
为什么要用适配器做成定制化?你知道的,UI那边总是动不动就改的,加点什么啊,删点什么啊,而且有多态机器需要适配,底色不一样就算了,数量和Item也不一样,怎么搞啊?我们总不能每一次都改一大串吧,改一大串和重复类似工作对我们来说简直就是折磨,所以,需要定制化。当然,如果第二次就直接淘汰圆盘了,那另当别论,。
工程代码:https://github.com/aknew123/CircleMenu 点击打开链接
效果如下:
程序架构的UML图,如下:
一、圆盘菜单自定义控件的使用
网上查看了一下,看一下他们的实现方式千篇一律,功能都写在一个文件里,阅读难度稍大,于是采用适配器模式做一个,把view和实现逻辑分离,就是简单的MVC,代码结构如下图:
当然,这只是一个Library,把DefaultMenuAdapter删了,重新编译就可以直接用jar包了,测试模块写在另一个工程,使用示例如下:
package com.example.circlemenutest; import java.util.ArrayList; import java.util.List; import android.animation.ObjectAnimator; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageView; import android.widget.Toast; import com.pan.tanglang.circlemenu.model.CircleMenuStatus; import com.pan.tanglang.circlemenu.view.CircleMenu; import com.pan.tanglang.circlemenu.view.CircleMenu.OnMenuItemClickListener; import com.pan.tanglang.circlemenu.view.CircleMenu.OnMenuStatusChangedListener; public class MainActivity extends Activity { public static final String TAG = "MainActivity"; private String[] mItemTexts = new String[] { "安全中心", "特殊服务", "投资理财", "转账汇款", "我的账户", "信用卡", "腾讯", "阿里", "百度" }; private int[] mItemImgs = new int[] { R.drawable.foreign01, R.drawable.foreign02, R.drawable.foreign03, R.drawable.foreign04, R.drawable.foreign05, R.drawable.foreign06, R.drawable.foreign07, R.drawable.foreign08, R.drawable.foreign09 }; private CircleMenu mCircleMenu; private ImageView ivCenter; private float startRotate; private float startFling; ObjectAnimator animRotate = null; ObjectAnimator animFling = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mCircleMenu = (CircleMenu) findViewById(R.id.cm_main); ivCenter = (ImageView) findViewById(R.id.iv_center_main); ivCenter.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, "圆盘中心", Toast.LENGTH_SHORT).show(); } }); mCircleMenu.setOnItemClickListener(new OnMenuItemClickListener() { @Override public void onClick(View view, int position) { Toast.makeText(MainActivity.this, mItemTexts[position], Toast.LENGTH_SHORT).show(); } }); mCircleMenu.setOnStatusChangedListener(new OnMenuStatusChangedListener() { @Override public void onStatusChanged(CircleMenuStatus status, double rotateAngle) { // TODO 可在此处定制各种动画 odAnimation(status, (float)rotateAngle); } }); List<ItemInfo> data = new ArrayList<>(); ItemInfo item = null; for (int i = 0; i < mItemTexts.length; i++) { item = new ItemInfo(mItemImgs[i], mItemTexts[i]); data.add(item); } mCircleMenu.setAdapter(new CircleMenuAdapter(data)); } private void odAnimation(CircleMenuStatus status, float rotateAngle) { switch (status) { case IDLE: Log.i(TAG, "--- -IDLE-----"); animRotate.cancel(); animRotate.cancel(); break; case START_ROTATING: Log.i(TAG, "--- -START_ROTATING-----"); break; case ROTATING: animRotate = ObjectAnimator.ofFloat(ivCenter, "rotation", startRotate, startRotate + rotateAngle); animRotate.setDuration(200).start(); startRotate += rotateAngle; // Log.i(TAG, "--- -ROTATING-----"); break; case STOP_ROTATING: Log.i(TAG, "--- -STOP_ROTATING-----"); break; case START_FLING: Log.i(TAG, "--- -START_FLING-----"); break; case FLING: // Log.i(TAG, "--- -FLING-----"); animFling = ObjectAnimator.ofFloat(ivCenter, "rotation", startFling, startFling + rotateAngle); animFling.setDuration(200).start(); startFling += rotateAngle; break; case STOP_FLING: Log.i(TAG, "--- -STOP_FLING-----"); break; default: break; } } }
后面的是用户定制化动画实现(提供诸多状态,爱怎么折腾怎么折腾)。好了,我们来看看实现原理。
二、实现原理
1.先看需要做什么
(1).圆盘菜单CircleMenu是一个转盘,装有各种Item,是一个容器,那理所当然是继承ViewGroup,为了方便实现飞转,所以监听用户手势OnGestureListener;
(2).菜单项CircleItemView,虽是一个item,但为了可定制化,当然也是一个容器,也是为了方便实现飞转,重写onFling方法,所以这里继承LinearLayout,纯属为了方便布局,若有特殊需求,可在布局的时候,在外层添加一个FrameLayout容器,爱怎么搞怎么搞;
(3).Adapter和Model,既然有Item那肯定也有Adapter和数据model,Adapter继承BaseAdapter即可,就跟ListView一样。
好了,就这3个玩意儿,另外几个都是从上面这两个抽出来的。
2.旋转原理
来看一下转动分析图,
图中圆心的坐标应为( mRadius, mRadius),则圆盘半径为mRadius,按照大多数人的习惯右手向下滑动,圆盘也就跟着顺时针转动(或者说滚动),当然不是圆盘在转,是菜单项在滚动,滚动了弧度为a,那途中Item的x、y坐标应为
x = tmp *cos a;
y = tmp*sin a;
这是相对于圆心的坐标,再加上圆盘的半径,就是圆盘中的坐标了,子View只管在父容器中的坐标,父容器布局时会加上自身的left和top,这样逐层往上推就是在屏幕中的坐标了)。
我们将Item强制为正方形,itemWidth为Item的宽度,当 tmp = mRaiuds - itemWidth / Math.sqrt(2) 时,那Item的外直角就走在圆盘圆周上,再大则要出边界了,所以,Item的中心点位于mRaiuds/2和mRaiuds - itemWidth / Math.sqrt(2)之间最为合理,那么,item的x、y为
final int childCount = getChildCount(); int left, top, halfDiagonal; // 限制Item的宽高 int itemWidth = (int) (mRadius * RADIO_DEFAULT_CHILD_DIMENSION); float angleDelay = 360 / childCount; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == View.GONE) { continue; } mStartAngle %= 360; // 取Item对角线的一半为Item中心到圆盘圆周的距离 halfDiagonal = (int) (itemWidth / Math.sqrt(2)); float distanceFromCenter = mRadius - halfDiagonal - mPadding; left = mRadius + (int) Math.round(distanceFromCenter * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f * itemWidth); top = mRadius + (int) Math.round(distanceFromCenter * Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f * itemWidth); // 重新Layout child.layout(left, top, left + itemWidth, top + itemWidth); mStartAngle += angleDelay; }
left 为 x, top 为y,mStartAngle为间隔角度,一个for循环就可以把所有Item按角度Layout出来。要让他们滚动起来,只要在ACTION_MOVE的时候给一个角度就行。那转动原理就这样了。
3.测量和布局
Android View 的绘制过程,如下图
过程为 measure() ---> layout () ---> draw(),measure() 中会掉用onMeasure()方法, layout () 中调用onLayout()方法,draw()我们就不管了,因为还是SDK继承而来,他可以自己绘制。
onMeasure方法实现如下:
@Override protected void o 11d41 nMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 测量自身 // measureMyself(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 测量子View measureChildViews(); } private void measureChildViews() { if (mAdapter.getCount() <= 0) { return; } // 获取半径, mRadius = Math.max(getMeasuredWidth(), getMeasuredHeight()) / 2; final int count = getChildCount(); // 取mRadius/2为Item宽度 int childSize = (int) (mRadius * RADIO_DEFAULT_CHILD_DIMENSION); int childMode = MeasureSpec.EXACTLY; int makeMeasureSpec = -1; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == View.GONE) { continue; } makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize, childMode); // 设置为正方形 child.measure(makeMeasureSpec, makeMeasureSpec); } // 取mRadius/10为默认内边距 if (mPadding == -1) { mPadding = RADIO_PADDING_LAYOUT * mRadius; } }
onLayout()在父类里是一个抽象方法,没有返回值,所以其中没有改变任何成员变量的话,是没有效果的。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mAdapter.getCount() <= 0) { return; } final int childCount = getChildCount(); int left, top, halfDiagonal; // 限制Item的宽高 int itemWidth = (int) (mRadius * RADIO_DEFAULT_CHILD_DIMENSION); float angleDelay = 360 / childCount; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == View.GONE) { continue; } mStartAngle %= 360; // 取Item对角线的一半为Item中心到圆盘圆周的距离 halfDiagonal = (int) (itemWidth / Math.sqrt(2)); float distanceFromCenter = mRadius - halfDiagonal - mPadding; left = mRadius + (int) Math.round(distanceFromCenter * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f * itemWidth); top = mRadius + (int) Math.round(distanceFromCenter * Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f * itemWidth); // 重新Layout child.layout(left, top, left + itemWidth, top + itemWidth); mStartAngle += angleDelay; } }onLayout里就是把所有的Iitem逐个布局,这样就显示一个静态的圆盘了。
4.圆盘滚动
圆盘的滚动,可以在onTouchEvent(MotionEvent event)方法里实现,但这意味着你要同时在CircleMenu和CircleItemView的onTouchEvent方法里都要实现,当然两套代码可以提出来(但有可能子View和父View的x、y坐标不同,有麻烦),由于我们知道Android事件分发机制,Touch事件首先是父View的dispatchTouchEvent(MotionEvent
event)先获得,然后往下分发,所以在dispatchTouchEvent方法实现Item转动一举两得,还有个原因是我们还要监听手势,需要重写onTouchEvent方法,so。。。。Item的滚动通过重写dispatchTouchEvent实现,代码如下:
// 因为父view的dispatchTouchEvent先获得事件,所以在这里可以连带子view的滚动事件一起处理 @Override public boolean dispatchTouchEvent(MotionEvent event) { if (mRotate != null) { mRotate.onCircleMenuTouch(event, mRadius, this); } return super.dispatchTouchEvent(event); }
简单吧,呵呵,提出来了,提到了RotateEngine.java里实现,如下:
package com.pan.tanglang.circlemenu.control; import android.util.Log; import android.view.MotionEvent; import com.pan.tanglang.circlemenu.model.CircleMenuStatus; import com.pan.tanglang.circlemenu.view.CircleMenu; import com.pan.tanglang.circlemenu.view.CircleMenu.RotateDirection; /** * 随手势旋转引擎 */ public class RotateEngine { public static final String TAG = "RotateEngine"; private static RotateEngine instance = null; private float startX; private float startY; /** 请求重新布局的起始角度 **/ private double mStartAngle; private RotateEngine() { } public static RotateEngine getInstance() { if (instance == null) { synchronized (RotateEngine.class) { if (instance == null) { instance = new RotateEngine(); } } } return instance; } // 通过dispatch实现 public void onCircleMenuTouch(MotionEvent ev, int radius, CircleMenu mCircleMenu) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: startX = ev.getX(); startY = ev.getY(); break; case MotionEvent.ACTION_MOVE: double start = getAngle(startX, startY, radius); if (mCircleMenu.getStatus() == CircleMenuStatus.STOP_FLING || mCircleMenu.getStatus() == CircleMenuStatus.IDLE) { // 开始滚动 mCircleMenu.startRotate(); // 拿到Fling停下(自然停或按停)之后的开始角度 mStartAngle = mCircleMenu.getmStartAngle(); } float x = ev.getX(); float y = ev.getY(); toCircleMenuScroll(mCircleMenu, radius, start, ev); startX = x; startY = y; break; case MotionEvent.ACTION_UP: CircleMenuStatus status = mCircleMenu.getStatus(); Log.i(TAG, "---ACTION_UP---status = " + status); if (status == CircleMenuStatus.ROTATING || status == CircleMenuStatus.PAUSE_ROTATING) { mCircleMenu.stopRotate(); } break; default: break; } } public void toCircleMenuScroll(CircleMenu mCircleMenu, int radius, double start, MotionEvent e2) { float x = e2.getX(); float y = e2.getY(); // 一个连续的角度差值,用于判断滑动方向,得出顺时针、逆时针 float directionAngle = 0; double end = getAngle(x, y, radius); // 如果是一、四象限,则直接end-start,角度值都是正值 if (getQuadrant(x, y, radius) == 1 || getQuadrant(x, y, radius) == 4) { mStartAngle += end - start; directionAngle += end - start; } else { // 二、三象限,角度值是负值 mStartAngle += start - end; directionAngle += start - end; } if (directionAngle > 0) {//得到旋转方向---顺时针,用于Fling飞转 mCircleMenu.setmDirection(RotateDirection.CLOCKWISE); } else { mCircleMenu.setmDirection(RotateDirection.ANTICLOCKWISE); } // 旋转角度, 请求重新布局 mCircleMenu.relayoutMenu(mStartAngle); if (startX != x || startY != y) { mCircleMenu.onRotating(directionAngle); } else { mCircleMenu.onPauseRotate(); } } public double getAngle(float xTouch, float yTouch, int radius) { double x = xTouch - radius; double y = yTouch - radius; return (double) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI); } public int getQuadrant(float x, float y, int radius) { int tmpX = (int) (x - radius); int tmpY = (int) (y - radius); if (tmpX >= 0) { return tmpY >= 0 ? 4 : 1; } else { return tmpY >= 0 ? 3 : 2; } } }
之所以代码这么多,是因为要处理诸多逻辑和诸多状态,主要思路就是得到x、y差值,计算角度差值,根据象限给加减角度,看ACTION_MOVE这段即可当然里面还牵扯到后面3个方法也差不多看完了。
5.圆盘飞转
飞转显然就是Fling,肯定是要在onFling()方法里面实现的,那么要实现OnGestureListener接口,同时,重写dispatchTouchEvent(MotionEvent event)方法,代码如下:
@Override public boolean onTouchEvent(MotionEvent event) { boolean result = mDetector.onTouchEvent(event); if (event.getAction() == MotionEvent.ACTION_UP) { if (mStatus != CircleMenuStatus.START_FLING && mStatus != CircleMenuStatus.FLING) { idle(); } } return result; }
再来看onFling方法
@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { startFling(e1, e2, velocityX, velocityY); return false; } /** * 开始飞转 * @param e1 * @param e2 * @param velocityX * @param velocityY */ public void startFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (mStatus == CircleMenuStatus.STOP_ROTATING) { startMenuFling(); float velocity = Math.abs(velocityX) > Math.abs(velocityY) ? velocityX : velocityY; double start = mRotate.getAngle(e2.getX(), e2.getY(), mRadius); if (mFlingEngine == null) { mFlingEngine = new FlingEngine(); } mFlingEngine.start(this, velocity, start); } }
也很简单,都提出来的,看FlingEngine.java。
package com.pan.tanglang.circlemenu.control; import com.pan.tanglang.circlemenu.model.CircleMenuStatus; import com.pan.tanglang.circlemenu.view.CircleMenu; import com.pan.tanglang.circlemenu.view.CircleMenu.RotateDirection; /** * @description 描 述:飞转引擎 */ public class FlingEngine implements Runnable { private static final String TAG = "FlingEngine"; private int mVelocity; private int DELAY = 10; private double startAngle; private CircleMenu mCircleMenu; public void start(CircleMenu circleMenu, float velocity, double start) { mCircleMenu = circleMenu; mVelocity = (int) Math.abs(velocity); startAngle = start; if (circleMenu.getStatus() == CircleMenuStatus.START_FLING) { circleMenu.post(this); } } @Override public void run() { // 如果小于20,则停止 if (mCircleMenu.getStatus() == CircleMenuStatus.STOP_FLING || mVelocity <= 0) { //叫停或自动停止 mCircleMenu.idle(); return; } double preStartAngle = startAngle; // 顺时针 if (mCircleMenu.getmDirection() == RotateDirection.CLOCKWISE) { flingSlowDownByClockwise(); } else if (mCircleMenu.getmDirection() == RotateDirection.ANTICLOCKWISE) { flingSlowDownByAnticlockwise(); } mCircleMenu.relayoutMenu(startAngle); mCircleMenu.onMenuFling(startAngle - preStartAngle); mCircleMenu.postDelayed(this, DELAY); } // 逆时针减速 private void flingSlowDownByAnticlockwise() { if (mVelocity > 10000) { mVelocity -= 1000; startAngle -= 10; } else if (mVelocity > 5000) { mVelocity -= 100; startAngle -= 8; } else if (mVelocity > 1000) { mVelocity -= 50; startAngle -= 6; } else if (mVelocity > 500) { mVelocity -= 10; startAngle -= 4; } else if (mVelocity > 100) { mVelocity -= 5; startAngle -= 2; } else { mVelocity--; startAngle--; } } // 顺时针减速 private void flingSlowDownByClockwise() { if (mVelocity > 10000) { mVelocity -= 1000; startAngle += 10; } else if (mVelocity > 5000) { mVelocity -= 100; startAngle += 8; } else if (mVelocity > 1000) { mVelocity -= 50; startAngle += 6; } else if (mVelocity > 500) { mVelocity -= 10; startAngle += 4; } else if (mVelocity > 100) { mVelocity -= 5; startAngle += 2; } else { mVelocity--; startAngle++; } } public int getQuadrant(float angle, int radius) { float x = Math.round(radius * Math.cos(Math.toRadians(angle))); float y = Math.round(radius * Math.sin(Math.toRadians(angle))); int tmpX = (int) (x - radius / 2); int tmpY = (int) (y - radius / 2); if (tmpX >= 0) { return tmpY >= 0 ? 4 : 1; } else { return tmpY >= 0 ? 3 : 2; } } }
之所以要提出startFling方法是因为Item也要用到,通过getParent强转之后调用,所以,CircleItemView.java的代码就相当简单了,如下:
package com.pan.tanglang.circlemenu.view; import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.View; import android.view.GestureDetector.OnGestureListener; import android.view.MotionEvent; import android.widget.LinearLayout; import com.pan.tanglang.circlemenu.model.CircleMenuStatus; import com.pan.tanglang.circlemenu.model.UserEvent; /** * @description 描 述:Item项自定义控件,主要是为了实现Item的onFling */ public class CircleItemView extends LinearLayout implements OnGestureListener { public static final String TAG = "CircleItemView"; private GestureDetector mDetector; private CircleMenu mParent; public CircleItemView(Context context, AttributeSet attrs) { super(context, attrs); setClickable(true); mDetector = new GestureDetector(context, this); } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { mDetector.onTouchEvent(event); //将滑动状态从 STOP_ROTATING 置为 IDLE,多一个STOP_ROTATING,是ROTATING之后,TART_FLING之前的一个状态,目前意义不大,主要看用户怎么利用 if (event.getAction() == MotionEvent.ACTION_UP) { CircleMenuStatus status = mParent.getStatus(); if (status != CircleMenuStatus.IDLE &&status != CircleMenuStatus.START_FLING && status != CircleMenuStatus.FLING) { mParent.idle(); } } return super.onTouchEvent(event);// 防止点击事件丢失 } @Override public boolean onDown(MotionEvent e) { if (mParent == null) { mParent = (CircleMenu) getParent(); } if (mParent != null) { CircleMenuStatus status = mParent.getStatus(); if (status == CircleMenuStatus.FLING) { mParent.stopFling(); } if (status == CircleMenuStatus.ROTATING) { mParent.stopRotate(); } } return true; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (mParent != null) { mParent.startFling(e1, e2, velocityX, velocityY); } return false; } }
CircleMenu.java的代码就不贴出来了,上https://github.com/aknew123/CircleMenu上downloade吧,里面有状态控制,其实代码也不到400行,只是贴进来显得文长(想想还是贴吧)。
package com.pan.tanglang.circlemenu.view; import android.annotation.SuppressLint; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ListAdapter; import com.pan.tanglang.circlemenu.control.FlingEngine; import com.pan.tanglang.circlemenu.control.RotateEngine; import com.pan.tanglang.circlemenu.model.CircleMenuStatus; import com.pan.tanglang.circlemenu.model.UserEvent; /** * @description 描 述:圆盘菜单容器,圆盘菜单功能的核心文件 * @author 作 者:SergioPan */ public class CircleMenu extends ViewGroup implements OnGestureListener { public static final String TAG = "CircleMenu"; /** 圆盘半径,那么圆心为(mRadius, mRadius) **/ public int mRadius; private static final float RADIO_DEFAULT_CHILD_DIMENSION = 1 / 2f; private static final float RADIO_PADDING_LAYOUT = 1 / 20f; /** 内边距,默认为mRadius/20 **/ public float mPadding = -1; private double mStartAngle = 0; private OnMenuItemClickListener mListener; private ListAdapter mAdapter; private CircleMenuStatus mStatus = CircleMenuStatus.IDLE; private OnMenuStatusChangedListener mStatusListener; private GestureDetector mDetector; private RotateEngine mRotate; private RotateDirection mDirection = RotateDirection.CLOCKWISE; private FlingEngine mFlingEngine; // 用户输入事件,默认为无用行为,主要用于解决Fling后的按停,此时屏蔽点击事件 private UserEvent mUserEvent = UserEvent.USELESS_ACTION; public CircleMenu(Context context, AttributeSet attrs) { super(context, attrs); setPadding(0, 0, 0, 0); setClickable(true); mDetector = new GestureDetector(context, this); mRotate = RotateEngine.getInstance(); } /** 转动方向 **/ public enum RotateDirection { /** 顺时针 **/ CLOCKWISE, /** 逆时针 **/ ANTICLOCKWISE; } //依附到窗口上 @Override protected void onAttachedToWindow() { if (mAdapter != null) { buildMenuItems(); } super.onAttachedToWindow(); } /** * 菜单重新布局 * * @param startAngle */ public void relayoutMenu(double startAngle) { mStartAngle = startAngle; requestLayout(); } // 构建菜单项 @SuppressLint("NewApi") private void buildMenuItems() { if (mAdapter.getCount() <= 0) { return; } for (int i = 0; i < mAdapter.getCount(); i++) { CircleItemView itemView = (CircleItemView) mAdapter.getView(i, null, this); final int position = i; itemView.setClickable(true); itemView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mUserEvent == UserEvent.FLING) { // 正在飞转时,接收用户的点击事件,则视为停止飞转动作,而屏蔽点击事件 mUserEvent = UserEvent.USELESS_ACTION; return; } // 非飞转时响应点击事件 if (mListener != null) { mListener.onClick(v, position); } } }); addView(itemView); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 测量自身 // measureMyself(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 测量子View measureChildViews(); } private void measureChildViews() { if (mAdapter.getCount() <= 0) { return; } // 获取半径, mRadius = Math.max(getMeasuredWidth(), getMeasuredHeight()) / 2; final int count = getChildCount(); // 取mRadius/2为Item宽度 int childSize = (int) (mRadius * RADIO_DEFAULT_CHILD_DIMENSION); int childMode = MeasureSpec.EXACTLY; int makeMeasureSpec = -1; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == View.GONE) { continue; } makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize, childMode); // 设置为正方形 child.measure(makeMeasureSpec, makeMeasureSpec); } // 取mRadius/10为默认内边距 if (mPadding == -1) { mPadding = RADIO_PADDING_LAYOUT * mRadius; } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mAdapter.getCount() <= 0) { return; } final int childCount = getChildCount(); int left, top, halfDiagonal; // 限制Item的宽高 int itemWidth = (int) (mRadius * RADIO_DEFAULT_CHILD_DIMENSION); float angleDelay = 360 / childCount; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); if (child.getVisibility() == View.GONE) { continue; } mStartAngle %= 360; // 取Item对角线的一半为Item中心到圆盘圆周的距离 halfDiagonal = (int) (itemWidth / Math.sqrt(2)); float distanceFromCenter = mRadius - halfDiagonal - mPadding; left = mRadius + (int) Math.round(distanceFromCenter * Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f * itemWidth); top = mRadius + (int) Math.round(distanceFromCenter * Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f * itemWidth); // 重新Layout child.layout(left, top, left + itemWidth, top + itemWidth); mStartAngle += angleDelay; } } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { boolean result = mDetector.onTouchEvent(event); if (event.getAction() == MotionEvent.ACTION_UP) { if (mStatus != CircleMenuStatus.START_FLING && mStatus != CircleMenuStatus.FLING) { idle(); } } return result; } // 因为父view的dispatchTouchEvent先获得事件,所以在这里可以连带子view的滚动事件一起处理 @Override public boolean dispatchTouchEvent(MotionEvent event) { if (mRotate != null) { mRotate.onCircleMenuTouch(event, mRadius, this); } return super.dispatchTouchEvent(event); } public void startMenuFling() { mStatus = CircleMenuStatus.START_FLING; if (mStatusListener != null) { mStatusListener.onStatusChanged(mStatus, 0); } } public void stopFling() { mStatus = CircleMenuStatus.STOP_FLING; if (mStatusListener != null) { mStatusListener.onStatusChanged(mStatus, 0); } } public void onMenuFling(double angle) { mStatus = CircleMenuStatus.FLING; if (mStatusListener != null) { mStatusListener.onStatusChanged(mStatus, angle); mUserEvent = UserEvent.FLING; } } public void startRotate() { mStatus = CircleMenuStatus.START_ROTATING; if (mStatusListener != null) { mStatusListener.onStatusChanged(mStatus, 0); } } public void onRotating(float angle) { mStatus = CircleMenuStatus.ROTATING; if (mStatusListener != null) { mStatusListener.onStatusChanged(mStatus, angle); } } public void onPauseRotate() { mStatus = CircleMenuStatus.PAUSE_ROTATING; if (mStatusListener != null) { mStatusListener.onStatusChanged(mStatus, 0); } } public void stopRotate() { mStatus = CircleMenuStatus.STOP_ROTATING; if (mStatusListener != null) { mStatusListener.onStatusChanged(mStatus, 0); } } public void idle() { mStatus = CircleMenuStatus.IDLE; if (mStatusListener != null) { mStatusListener.onStatusChanged(mStatus, 0); } } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return true; } @Override public boolean onDown(MotionEvent e) { if (mStatus == CircleMenuStatus.FLING) { stopFling(); } return true; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return true; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { startFling(e1, e2, velocityX, velocityY); return false; } /** * 开始飞转 * @param e1 * @param e2 * @param velocityX * @param velocityY */ public void startFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (mStatus == CircleMenuStatus.STOP_ROTATING) { startMenuFling(); float velocity = Math.abs(velocityX) > Math.abs(velocityY) ? velocityX : velocityY; double start = mRotate.getAngle(e2.getX(), e2.getY(), mRadius); if (mFlingEngine == null) { mFlingEngine = new FlingEngine(); } mFlingEngine.start(this, velocity, start); } } public RotateDirection getmDirection() { return mDirection; } public void setmDirection(RotateDirection mDirection) { this.mDirection = mDirection; } public void setStatus(CircleMenuStatus status) { mStatus = status; } public CircleMenuStatus getStatus() { return mStatus; } public void setAdapter(ListAdapter adapter) { mAdapter = adapter; } public ListAdapter getAdapter() { return mAdapter; } public void setOnItemClickListener(OnMenuItemClickListener listener) { this.mListener = listener; } public void setOnStatusChangedListener(OnMenuStatusChangedListener statusListener) { this.mStatusListener = statusListener; } public UserEvent getmUserEvent() { return mUserEvent; } public void setmUserEvent(UserEvent mUserEvent) { this.mUserEvent = mUserEvent; } public double getmStartAngle() { return mStartAngle; } public float getmPadding() { return mPadding; } public void setmPadding(float mPadding) { this.mPadding = mPadding; } /** * 圆盘菜单状态改变监听器,用于定制外围或圆心动画 * * @author SergioPan */ public interface OnMenuStatusChangedListener { /** * @param status * 状态 * @param rotateAngle * 旋转量(角度) */ public void onStatusChanged(CircleMenuStatus status, double rotateAngle); } /** * Item点击事件监听器 * * @author SergioPan */ public interface OnMenuItemClickListener { public void onClick(View view, int position); } }
下面说说适配器模式,按开发的角度,应该一开始就该说了,要不怎么调试啊,好!马上来!
6.适配器模式
想要定制化就要用适配器模式,这样才人性化,要不只能写一些只能用一次的逻辑代码而已。适配器模式也是一个小的MVC模式,
Modle ----- DefaultItem
View ---- CircleItemView
Controller ---- Adapter
用起来,其实挺简单,当然这是简单的MVC,父容器CircleMenu要实现2个方法,如下:
public void setAdapter(ListAdapter adapter) { mAdapter = adapter; }
//依附到窗口上 @Override protected void onAttachedToWindow() { if (mAdapter != null) { buildMenuItems(); } super.onAttachedToWindow(); }
// 构建菜单项 @SuppressLint("NewApi") private void buildMenuItems() { if (mAdapter.getCount() <= 0) { return; } for (int i = 0; i < mAdapter.getCount(); i++) { CircleItemView itemView = (CircleItemView) mAdapter.getView(i, null, this); final int position = i; itemView.setClickable(true); itemView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mUserEvent == UserEvent.FLING) { // 正在飞转时,接收用户的点击事件,则视为停止飞转动作,而屏蔽点击事件 mUserEvent = UserEvent.USELESS_ACTION; return; } // 非飞转时响应点击事件 if (mListener != null) { mListener.onClick(v, position); } } }); addView(itemView); } }
其实就是装饰(装饰模式)了一下Adapter,然后调用Adapter的getView方法,即
mAdapter.getView(i, null, this);
然后,Adapter就更简单了,跟ListView一样,继承BaseAdapter,再重写这4个方法,DefaultMenuAdapter.java代码如下
package com.pan.tanglang.circlemenu.adapter; import java.util.List; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import com.pan.tanglang.circlemenu.R; import com.pan.tanglang.circlemenu.model.DefaultItem; /** * @description 描 述:圆盘菜单默认适配器,如有其他需求,用户自行定义适配器,继承BaseAdapter就可以,自行编写xml文件 */ public class DefaultMenuAdapter extends BaseAdapter { private List<DefaultItem> items; public DefaultMenuAdapter(List<DefaultItem> data) { items = data; } @Override public int getCount() { if (items == null) { return 0; } return items.size(); } @Override public Object getItem(int position) { return items.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if (convertView == null) { convertView = View.inflate(parent.getContext(), R.layout.item_default, null); holder = new ViewHolder(); holder.iv = (ImageView) convertView.findViewById(R.id.iv_default_circle_menu_item); holder.tv = (TextView) convertView.findViewById(R.id.tv_default_circle_menu_item); convertView.setTag(holder); } holder = (ViewHolder) convertView.getTag(); DefaultItem item = items.get(position); if (item != null) { holder.iv.setImageResource(item.getImgId()); holder.tv.setText(item.getText()); } return convertView; } class ViewHolder { ImageView iv; TextView tv; } }
这样就做到,高度定制化。
7.状态模式定制动画
可根据圆盘状态,定制与之相匹配的动画,想要多炫有多炫,只要监听setOnStatusChangedListener方法即可实现。
public enum CircleMenuStatus { /** 静止 **/ IDLE, /** 开始旋转 **/ START_ROTATING, /** 正在旋转 **/ ROTATING, /** 暂停旋转 **/ PAUSE_ROTATING, /** 叫停旋转,是ROTATING之后,START_FLING之前的一个状态,本项目中意义不大,看用户怎么利用 **/ STOP_ROTATING, /** 开始飞转 **/ START_FLING, /**正在飞转 **/ FLING, /** 叫停飞转 **/ STOP_FLING; }
mCircleMenu.setOnStatusChangedListener(new OnMenuStatusChangedListener() { @Override public void onStatusChanged(CircleMenuStatus status, double rotateAngle) { // TODO 可在此处定制各种动画 odAnimation(status, (float)rotateAngle); } }); private void odAnimation(CircleMenuStatus status, float rotateAngle) { switch (status) { case IDLE: Log.i(TAG, "--- -IDLE-----"); animRotate.cancel(); animRotate.cancel(); break; case START_ROTATING: Log.i(TAG, "--- -START_ROTATING-----"); break; case ROTATING: animRotate = ObjectAnimator.ofFloat(ivCenter, "rotation", startRotate, startRotate + rotateAngle); animRotate.setDuration(200).start(); startRotate += rotateAngle; // Log.i(TAG, "--- -ROTATING-----"); break; case STOP_ROTATING: Log.i(TAG, "--- -STOP_ROTATING-----"); break; case START_FLING: Log.i(TAG, "--- -START_FLING-----"); break; case FLING: // Log.i(TAG, "--- -FLING-----"); animFling = ObjectAnimator.ofFloat(ivCenter, "rotation", startFling, startFling + rotateAngle); animFling.setDuration(200).start(); startFling += rotateAngle; break; case STOP_FLING: Log.i(TAG, "--- -STOP_FLING-----"); break; default: break; } }
是不是很炫啊
完!
相关文章推荐
- Android 高仿【优酷】圆盘旋转菜单的实现(转载)
- Android 高仿【优酷】圆盘旋转菜单 的实现
- Android 高仿【优酷】圆盘旋转菜单的实现
- Android 高仿【优酷】圆盘旋转菜单的实现 .
- Android 高仿【优酷】圆盘旋转菜单的实现(转载)
- Android 高仿【优酷】圆盘旋转菜单的实现
- Android 高仿【优酷】圆盘旋转菜单 的实现
- Android 高仿【优酷】圆盘旋转菜单的实现
- Android 高仿【优酷】圆盘旋转菜单 的实现
- Android 高仿【优酷】圆盘旋转菜单的实现
- 【Android基础】Android 高仿【优酷】圆盘旋转菜单的实现
- 超炫圆形旋转菜单Android-CircleMenu
- android圆形旋转菜单,而对于移动转换功能支持
- Android 实现Path2.0中绚丽的的旋转菜单
- Android之——史上最简单旋转菜单实现效果
- Android 高仿优酷旋转菜单
- Android开发之自定义控件-YouKu圆盘菜单
- Android编程实现仿优酷旋转菜单效果(附demo源码)
- Android自定义控件——仿优酷圆盘菜单
- Android自定义控件——仿优酷圆盘菜单