首先想要說明一下,這個Demo例子是從eoeAndroid上面Download下來的,本文裡只是解析,學習一下實現原理。從昨天開始就想分析下,一直拖到今天,不到5點,睡不著了(當然不是因為這個技術問題),就趁著早晨把他寫下來吧,多有不足,請多多原諒。
下面開始正題 ,先看下程式運行是圖片:
然後你可以拖動圓形菜單外面項到圓形菜單中:
開始正式的代碼解析:
主Activity的onCreate:
@Overridepublic 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;}}}}
設定好各項參數,就應該繪製:
@Overridepublic 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);}
下面就是觸發事件的處理:
@Overridepublic 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