圆形可滑动菜单(可以动态添加菜单项)

  首先想要说明一下,这个Demo例子是从eoeAndroid上面Download下来的,本文里只是解析,学习一下实现原理。从昨天开始就想分析下,一直拖到今天,不到5点,睡不着了(当然不是因为这个技术问题),就趁着早晨把他写下来吧,多有不足,请多多原谅。

下面开始正题 ,先看下程序运行是图片:



然后你可以拖动圆形菜单外面项到圆形菜单中:



开始正式的代码解析:

主Activity的onCreate:

@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		this.requestWindowFeature(Window.FEATURE_NO_TITLE);
		getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
				WindowManager.LayoutParams.FLAG_FULLSCREEN);
		mNewView = new NewView(getApplicationContext(), 300, 200, 150);
		setContentView(mNewView);
		new Thread(new myThread()).start();
	}

里面自定义了一个NewView类,继承自View,就是我们第一张图显示的内容,然后又起了一个线程:

class myThread implements Runnable {
		public void run() {
			while (!Thread.currentThread().isInterrupted()) {
				Message message = new Message();
				switch (mNewView.getReturn()) {
				case 1:
					message.what = 0x101;
					break;
				case 2:
					message.what = 0x102;
					break;
				case 3:
					message.what = 0x103;
					break;
				case 4:
					message.what = 0x104;
					break;
				case 5:
					message.what = 0x105;
					break;
				case 6:
					message.what = 0x106;
					break;
				case 7:
					message.what = 0x107;
					break;
				case 8:
					message.what = 0x108;
					break;
				case 9:
					message.what = 0x109;
					break;
				}
				QSA.this.myHandler.sendMessage(message);
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					Thread.currentThread().interrupt();
				}
			}

这个线程就是监听图片有滑动的时候,通过Handler发送一个消息到UI线程,进行相对应的操作:

Handler myHandler = new Handler() {
		public void handleMessage(Message msg) {
//			newView.getReturn = -1;
			switch (msg.what) {
			case 0x101:
				break;			
			case 0x102:
				break;
			case 0x103:
				mIntent = new Intent(getApplicationContext(), ShowPic.class);
				startActivity(mIntent);
				break;
			case 0x104:
				break;
			case 0x105:
				break;
			case 0x106:
				break;
			case 0x107:
				mIntent = new Intent(getApplicationContext(), RunLed.class);
				startActivity(mIntent);
				break;
			case 0x108:
				break;
			case 0x109:
				break;
			}

			super.handleMessage(msg);
		}
	};

其主要内容在与NewView类,下面我们看下这个类:
public class NewView extends View {
	//返回值,用于调用者获取newView状态
	protected static int mGetReturn = -1;
	private Paint mPaint = new Paint();
	// stone列表
	private BigStone[] mStones;
	int mode = NONE;
	static final int NONE = 0;
	//拖动
	static final int DRAG = 1;
	//用于双指视图缩放
	static final int ZOOM = 2;
	//存放数据的数组
	private BigStone[] mMenus;
	private BigStone[] mAddMenus = new BigStone[MENUS];
	// 数目
	private static int STONE_COUNT = 5;
	private static int MENUS = 4;
	// 圆心坐标
	private float mPointX = 0, mPointY = 0;
	private int flagwai = 0;
	private int flag = 0;
	// 半径
	private int mRadius = 0;
	// 每两个点间隔的角度
	private int mDegreeDelta;
	private float maxX, maxY, minX, minY;

上面是一些全局变量,都加上了注释,比较好理解。

看它的构造方法:

public NewView(Context context, int px, int py, int radius) {
		super(context);
		//为圆心和半径赋值
		mPointX = px;
		mPointY = py;
		mRadius = radius;

		setBackgroundResource(R.drawable.menubkground);
		//设置菜单项(为两个菜单项赋值)
		setupStones();
		//计算坐标
		computeCoordinates();
	}

先看一下setupStone方法:

private void setupStones() {
		mStones = new BigStone[STONE_COUNT];
		mMenus = new BigStone[MENUS];
		BigStone stone;
		BigStone menus;
		//初始角度
		int angle = 0;
		//每项之间相隔角度
		mDegreeDelta = 360 / STONE_COUNT;
		//初始圆形菜单外的项
		if (flagwai == 0) {
			for (int i = 0; i < MENUS; i++) {
				menus = new BigStone();
				menus.bitmap = BitmapFactory.decodeResource(getResources(),
						R.drawable.menu6 + i);
				menus.text = String.valueOf(1 + i);
				mMenus[i] = menus;
			}
		}
		//初始化圆形菜单项
		for (int index = 0; index < STONE_COUNT; index++) {
			stone = new BigStone();
			stone.angle = angle;
			stone.bitmap = BitmapFactory.decodeResource(getResources(),
					R.drawable.menu1 + index);
			stone.text = String.valueOf(1 + index);
			//每一项的角度
			angle += mDegreeDelta;

			mStones[index] = stone;
		}
	}

里面该加注释的地方,我都加了注释,比较简单,不多说了,里面用到了一个BigStone的实体类:

class BigStone {
		// 图片
		Bitmap bitmap;
		// 角度
		int angle;
		// x坐标
		float x;
		// y坐标
		float y;
		String text;
		// 是否可见
		boolean isVisible = true;
	}

用于存储每一项的各个数据。

computeCoordinates,计算每一项的坐标:

private void computeCoordinates() {
		BigStone stone;
		BigStone menus;
		for (int index = 0; index < STONE_COUNT; index++) {
			stone = mStones[index];
			stone.x = mPointX
					+ (float) (mRadius * Math.cos(stone.angle * Math.PI / 180));
			stone.y = mPointY
					+ (float) (mRadius * Math.sin(stone.angle * Math.PI / 180));
		}
		if (flag == 0) {
			for (int i = 0; i < MENUS; i++) {
				menus = mMenus[i];
				switch (i) {
				case 0:
					menus.x = 300 * 1.8f;
					menus.y = 50;
					break;
				case 1:
					menus.x = 300 * 1.8f + 100;
					menus.y = 50;
					break;
				case 2:
					menus.x = 300 * 1.8f + 200;
					menus.y = 50;
					break;
				case 3:
					menus.x = 300 * 1.8f;
					menus.y = 150;
					break;
					//MENUS设置为4,下面这个应该是多余的
				case 4:
					menus.x = 300 * 1.8f + 10 + 100;
					menus.y = 250;
					break;

				}
			}
		}
	}

设置好各项参数,就应该绘制:

@Override
	public void onDraw(Canvas canvas) {
		Paint paint = new Paint();
		paint.setAntiAlias(true);
		paint.setColor(Color.WHITE);
		paint.setStyle(Paint.Style.FILL);
		paint.setAlpha(0x30);
		if (change == 0) {
			//绘制中心小圆
			canvas.drawCircle(mPointX, mPointY, mRadius - 80, paint);
		}
		paint.setStyle(Paint.Style.FILL);
		paint.setAntiAlias(true);
		paint.setColor(Color.BLUE);
		paint.setAlpha(0x30);
		if (change == 0) {
			canvas.drawCircle(mPointX, mPointY, mRadius + 41, paint); // 大圆
		}

		if (change == 1) {
			//满足一个条件不绘制圆盘菜单,绘制矩形菜单。
			canvas.drawRect(800, 240, 0, 140, paint);
		}
		//绘制每一个外部菜单项
		for (int i = 0; i < MENUS; i++) {
			if (!mMenus[i].isVisible)
				continue;
			drawMenus(canvas, mMenus[i].bitmap, mMenus[i].x, mMenus[i].y);
		}
		//绘制每一个园内菜单
		for (int index = 0; index < STONE_COUNT; index++) {
			if (!mStones[index].isVisible)
				continue;
			drawInCenter(canvas, mStones[index].bitmap, mStones[index].x,
					mStones[index].y, mStones[index].text);

		}
	}

先绘制小圆,然后是大圆:


绘制菜单项时,调用了drawMenus和drawInCenter方法:

void drawMenus(Canvas canvas, Bitmap b, float x, float y) {
		canvas.drawBitmap(b, x - b.getWidth() / 2, y - b.getHeight() / 2, null); // 图标
	}

	void drawInCenter(Canvas canvas, Bitmap bitmap, float left, float top,
			String text) {
		canvas.drawText(text, left, top, mPaint);
		canvas.drawBitmap(bitmap, left - bitmap.getWidth() / 2,
				top - bitmap.getHeight() / 2, null);
	}

下面就是触发事件的处理:

@Override
	public boolean dispatchTouchEvent(MotionEvent e) {
		dumpEvent(e);
		switch (e.getAction() & MotionEvent.ACTION_MASK) {
		case MotionEvent.ACTION_DOWN:
			//判断选中的是哪个点
			for (int i = 0; i < STONE_COUNT; i++) {
				if (e.getX() >= mStones[i].x - 20
						&& e.getX() <= mStones[i].x + 40
						&& e.getY() >= mStones[i].y
						&& e.getY() <= mStones[i].y + 40) {

					if (i < 5) {
						mGetReturn = Integer.valueOf(mStones[i].text);
					} else {
						mGetReturn = Integer.valueOf(mStones[i].text) + 5;
					}
					Toast.makeText(getContext(), String.valueOf(mGetReturn),
							Toast.LENGTH_SHORT).show();

				}
			}
			//把mode设为拖动
			mode = DRAG;
			return true;

		case MotionEvent.ACTION_POINTER_DOWN:
			//获取两点间的距离
			oldDist = spacing(e);
			if (oldDist > 100f) {
				mode = ZOOM;
			}
			return true;

		case MotionEvent.ACTION_MOVE:
			//获取最大最小坐标
			getMaxMin(e);
			if (mode == DRAG) {
			} else if (mode == ZOOM) {
				//获取两点间的距离
//				float newDist = spacing(e);
//				if (newDist > 100f) {
//					/*
//					 * if (change == 0) { change = 1;
//					 * 
//					 * resetStonesAngle(e.getX(), e.getY());
//					 * computeCoordinates(); invalidate(); }
//					 */
//				}
			}
			int a = 0;
			for (int i = 0; i < MENUS; i++) {
				if (e.getX() > mMenus[i].x - 40 && e.getX() < mMenus[i].x + 40
						&& e.getY() > mMenus[i].y - 40
						&& e.getY() < mMenus[i].y + 40) {
					mMenus[i].x = e.getX();
					mMenus[i].y = e.getY();
					flag = 1;
					computeCoordinates();
					//重新绘制
					postInvalidate();

					//从外面添加到圆形菜单中
					if (e.getX() < maxX && e.getX() > minX && e.getY() < maxY
							&& e.getY() > minY) {
						if (mMenus[i].isVisible) {
							for (int j = 0; j < MENUS; j++) {
								if (mAddMenus[j] == null && a == 0) {
									mAddMenus[j] = mMenus[i];
									a = 1;
								}
							}
							STONE_COUNT++;
							mDegreeDelta = 360 / STONE_COUNT;
							mStones = new BigStone[STONE_COUNT];
							flagwai = 1;
							int angle = 0;
							BigStone stone;
							for (int index = 0; index < STONE_COUNT; index++) {
								stone = new BigStone();

								if (index < 5) {
									stone.bitmap = BitmapFactory
											.decodeResource(getResources(),
													R.drawable.menu1 + index);
									stone.text = String.valueOf(1 + index);
								} else {
									stone.bitmap = mAddMenus[index - 5].bitmap;
									stone.text = mAddMenus[index - 5].text;
								}
								stone.angle = angle;
								angle += mDegreeDelta;
								mStones[index] = stone;
							}
							//把添加到圆内菜单项中的外部Item设为不可见,是不是可以考虑移除?
							
							mMenus[i].isVisible = false;
						}
					}
					break;
				}
			}

			if (e.getX() < maxX && e.getX() > minX && e.getY() < maxY
					&& e.getY() > minY) {
				if (e.getX() < maxX - 80 && e.getX() > minX + 81
						&& e.getY() < maxY - 80 && e.getY() > minY + 81) {
					mPointX = e.getX();
					mPointY = e.getY();
				}
				//重新设置每个item的角度
				resetStonesAngle(e.getX(), e.getY());
				//重新设置每一个item的坐标
				computeCoordinates();
				//重绘
				invalidate();
			}
			break;

		case MotionEvent.ACTION_UP:
			break;

		case MotionEvent.ACTION_POINTER_UP:
			mode = NONE;
			break;
		}
		return super.dispatchTouchEvent(e);
	}


先看以下事件:

  • MotionEvent.ACTION_DOWN:在第一个点被按下时触发
  • MotionEvent.ACTION_UP:当屏幕上唯一的点被放开时触发
  • MotionEvent.ACTION_POINTER_DOWN:当屏幕上已经有一个点被按住,此时再按下其他点时触发。
  • MotionEvent.ACTION_POINTER_UP:当屏幕上有多个点被按住,松开其中一个点时触发(即非最后一个点被放开时)。
  • MotionEvent.ACTION_MOVE:当有点在屏幕上移动时触发。值得注意的是,由于它的灵敏度很高,而我们的手指又不可能完全静止(即使我们感觉不到移动,但其实我们的手指也在不停地抖动),所以实际的情况是,基本上只要有点在屏幕上,此事件就会一直不停地被触发。

举例子来说:当我们放一个食指到屏幕上时,触发ACTION_DOWN事件;再放一个中指到屏幕上,触发ACTION_POINTER_DOWN事件;此时再把食指或中指放开,都会触发ACTION_POINTER_UP事件;再放开最后一个手指,触发ACTION_UP事件;而同时在整个过程中,ACTION_MOVE事件会一直不停地被触发。

逐个事件分析一下:

case MotionEvent.ACTION_DOWN:
			//判断选中的是哪个点
			for (int i = 0; i < STONE_COUNT; i++) {
				if (e.getX() >= mStones[i].x - 20
						&& e.getX() <= mStones[i].x + 40
						&& e.getY() >= mStones[i].y
						&& e.getY() <= mStones[i].y + 40) {

					if (i < 5) {
						mGetReturn = Integer.valueOf(mStones[i].text);
					} else {
						mGetReturn = Integer.valueOf(mStones[i].text) + 5;
					}
					Toast.makeText(getContext(), String.valueOf(mGetReturn),
							Toast.LENGTH_SHORT).show();

				}
			}
			//把mode设为拖动
			mode = DRAG;
			return true;

判断点击的是哪一项,然后弹出一个Toast提示。

case MotionEvent.ACTION_POINTER_DOWN:
			//获取两点间的距离
			oldDist = spacing(e);
			if (oldDist > 100f) {
				mode = ZOOM;
			}
			return true;

多点触发,这里只是算了一下距离,没做什么处理,如有需要,可以添加一些功能代码。里面用到了spacing方法:

private float spacing(MotionEvent event) {
		float x = event.getX(0) - event.getX(1);
		float y = event.getY(0) - event.getY(1);
		return FloatMath.sqrt(x * x + y * y);
	}

该方法用于获取两点间的距离。

move:

case MotionEvent.ACTION_MOVE:
			//获取最大最小坐标
			getMaxMin(e);
			if (mode == DRAG) {
			} else if (mode == ZOOM) {
				//获取两点间的距离
//				float newDist = spacing(e);
//				if (newDist > 100f) {
//					/*
//					 * if (change == 0) { change = 1;
//					 * 
//					 * resetStonesAngle(e.getX(), e.getY());
//					 * computeCoordinates(); invalidate(); }
//					 */
//				}
			}
			int a = 0;
			for (int i = 0; i < MENUS; i++) {
				if (e.getX() > mMenus[i].x - 40 && e.getX() < mMenus[i].x + 40
						&& e.getY() > mMenus[i].y - 40
						&& e.getY() < mMenus[i].y + 40) {
					mMenus[i].x = e.getX();
					mMenus[i].y = e.getY();
					flag = 1;
					computeCoordinates();
					//重新绘制
					postInvalidate();

					//从外面添加到圆形菜单中
					if (e.getX() < maxX && e.getX() > minX && e.getY() < maxY
							&& e.getY() > minY) {
						if (mMenus[i].isVisible) {
							for (int j = 0; j < MENUS; j++) {
								if (mAddMenus[j] == null && a == 0) {
									mAddMenus[j] = mMenus[i];
									a = 1;
								}
							}
							STONE_COUNT++;
							mDegreeDelta = 360 / STONE_COUNT;
							mStones = new BigStone[STONE_COUNT];
							flagwai = 1;
							int angle = 0;
							BigStone stone;
							for (int index = 0; index < STONE_COUNT; index++) {
								stone = new BigStone();

								if (index < 5) {
									stone.bitmap = BitmapFactory
											.decodeResource(getResources(),
													R.drawable.menu1 + index);
									stone.text = String.valueOf(1 + index);
								} else {
									stone.bitmap = mAddMenus[index - 5].bitmap;
									stone.text = mAddMenus[index - 5].text;
								}
								stone.angle = angle;
								angle += mDegreeDelta;
								mStones[index] = stone;
							}
							//把添加到圆内菜单项中的外部Item设为不可见,是不是可以考虑移除?
							
							mMenus[i].isVisible = false;
						}
					}
					break;
				}
			}

			if (e.getX() < maxX && e.getX() > minX && e.getY() < maxY
					&& e.getY() > minY) {
				if (e.getX() < maxX - 80 && e.getX() > minX + 81
						&& e.getY() < maxY - 80 && e.getY() > minY + 81) {
					mPointX = e.getX();
					mPointY = e.getY();
				}
				//重新设置每个item的角度
				resetStonesAngle(e.getX(), e.getY());
				//重新设置每一个item的坐标
				computeCoordinates();
				//重绘
				invalidate();
			}
			break;

里面就是各种判断,然后重新计算角度、坐标等,最后重新绘制。

里面有getMaxMin方法:

private void getMaxMin(MotionEvent e) {
		float tempx;
		float tempy;
		for (int i = 0; i < STONE_COUNT; i++) {
			for (int j = 0; j < STONE_COUNT; j++) {

				if (mStones[i].x < mStones[j].x) {
					tempx = mStones[i].x;
					mStones[i].x = mStones[j].x;
					mStones[j].x = tempx;
				}
				if (mStones[i].y < mStones[j].y) {
					tempy = mStones[i].y;
					mStones[i].y = mStones[j].y;
					mStones[j].y = tempy;
				}
			}
		}
		maxX = mStones[STONE_COUNT - 1].x;
		minX = mStones[0].x;
		maxY = mStones[STONE_COUNT - 1].y;
		minY = mStones[0].y;
	}

通过重新排序,获取最大最小坐标点。

里面的计算角度方法:

private void resetStonesAngle(float x, float y) {
		int angle = computeCurrentAngle(x, y);
		for (int index = 0; index < STONE_COUNT; index++) {
			mStones[index].angle = angle;
			angle += mDegreeDelta;
		}
	}

	private int computeCurrentAngle(float x, float y) {
		float distance = (float) Math
				.sqrt(((x - mPointX) * (x - mPointX) + (y - mPointY)
						* (y - mPointY)));
		int degree = (int) (Math.acos((x - mPointX) / distance) * 180 / Math.PI);
		if (y < mPointY) {
			degree = -degree;
		}
		return degree;
	}

好吧,就先写到这吧,饿了,先吃点东西。一会把代码上传上去,先吃点东西。



最后源代码下载地址:http://download.csdn.net/detail/aomandeshangxiao/4857216


  • 11
    点赞
  • 1
    收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 9

打赏作者

傲慢的上校

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值