圓形可滑動選單(可以動態新增選單項)

傲慢的上校發表於2012-12-07

  首先想要說明一下,這個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


相關文章