1. 程式人生 > >Android WheelMenu圓形選單,巧妙實現

Android WheelMenu圓形選單,巧妙實現

這個圓形選單是在GitHub開源專案Android-Wheel-Menu-master的基礎上修改而來

這個是GitHub專案的地址https://github.com/anupcowkur/Android-Wheel-Menu

不說別的了,直接上程式碼

1.自定義元件

package com.hisun.sinldo.consult.view;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ImageView;

public class WheelMenu extends ImageView {

	/** 原始和變數大小的影象 */
	private Bitmap imageOriginal, imageScaled; // variables for original and
												// re-sized image
	/** 矩陣進行旋轉 */
	private Matrix matrix; // Matrix used to perform rotations
	/** 檢視的高度和寬度 */
	private int wheelHeight, wheelWidth; // height and width of the view
	/** 車輪的頂部(在當前輪div計算) */
	private int top; // the current top of the wheel (calculated in
	// wheel divs)
	/**
	 * 變數計數總旋轉 一個給定的旋轉的車輪在 使用者(從action_down到action_up)
	 * <br/>
	 * 初始值為:-1 * (divAngle / 2);所以它是負值
	 */
	private double totalRotation; // variable that counts the total rotation
	// during a given rotation of the wheel by the
	// user (from ACTION_DOWN to ACTION_UP)
	/** 設定車輪的總份數 */
	private int divCount; // no of divisions in the wheel
	/** 每一份的角度 */
	private int divAngle; // angle of each division
	/** 目前由使用者選擇的部分。 */
	private int selectedPosition; // the section currently selected by the user.
	/** 變數決定是否折斷 */
	private boolean snapToCenterFlag = true; // variable that determines whether
												// to snap the
	// wheel to the center of a div or not
	private Context context;
	private WheelChangeListener wheelChangeListener;

	public WheelMenu(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context);
	}

	// initializations
	private void init(Context context) {
		this.context = context;
		this.setScaleType(ScaleType.MATRIX);
		selectedPosition = 0;

		// initialize the matrix only once
		// 初始化矩陣只有一次
		if (matrix == null) {
			matrix = new Matrix();
		} else {
			matrix.reset();
		}

		// touch events listener
//		this.setOnTouchListener(new WheelTouchListener());
	}

	/**
	 * Add a new listener to observe user selection changes.
	 * 
	 * @param wheelChangeListener
	 */
	public void setWheelChangeListener(WheelChangeListener wheelChangeListener) {
		this.wheelChangeListener = wheelChangeListener;
	}

	/**
	 * Returns the position currently selected by the user. 返回由使用者當前選擇的位置
	 * 
	 * @return the currently selected position between 1 and divCount.
	 *         當前選定的位置1和divcount之間。
	 */
	public int getSelectedPosition() {
		return selectedPosition;
	}

	/**
	 * Set no of divisions in the wheel menu. 沒有設定在車輪選單區劃。
	 * 
	 * @param divCount
	 *            no of divisions.
	 */
	public void setDivCount(int divCount) {
		this.divCount = divCount;

		divAngle = 360 / divCount;
		totalRotation = -1 * (divAngle / 2);
	}

	/**
	 * Set the snap to center flag. If true, wheel will always snap to center of
	 * current section. 設定捕捉中心標誌。如果是真的,車輪總是捕捉到當前截面中心。
	 * 
	 * @param snapToCenterFlag
	 */
	public void setSnapToCenterFlag(boolean snapToCenterFlag) {
		this.snapToCenterFlag = snapToCenterFlag;
	}

	/**
	 * Set a different top position. Default top position is 0.
	 * 設定不同的頂部位置。預設的頂部位置是0。 
	 * Should be set after {#setDivCount(int) setDivCount}
	 * method and the value should be greater than 0 and lesser than divCount,
	 * otherwise the provided value will be ignored. 
	 * 應設定在{#setDivCount(int) * setDivCount}和價值應大於0和小於divcount,否則所提供的值將被忽略。
	 * 
	 * @param newTopDiv
	 */
	public void setAlternateTopDiv(int newTopDiv) {

		if (newTopDiv < 0 || newTopDiv >= divCount)
			return;
		else
			top = newTopDiv;

		selectedPosition = top;
	}

	/**
	 * Set the wheel image.
	 * 
	 * @param drawableId
	 *            the id of the drawable to be used as the wheel image.
	 */
	public void setWheelImage(int drawableId) {
		imageOriginal = BitmapFactory.decodeResource(context.getResources(),
				drawableId);
	}

	/*
	 * We need this to get the dimensions of the view. Once we get those,
	 * 我們需要把檢視的尺寸。一旦我們得到這些, We can scale the image to make sure it's proper,
	 * 我們可以縮放圖片以確保它是正確的, Initialize the matrix and align it with the views
	 * center. 初始化矩陣,使其與檢視的中心。
	 */
	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);

		// method called multiple times but initialized just once
		// 方法呼叫多次但初始化一次
		if (wheelHeight == 0 || wheelWidth == 0) {
			wheelHeight = h;
			wheelWidth = w;
			// resize the image
			Matrix resize = new Matrix();
			resize.postScale((float)Math.min(wheelWidth, wheelHeight)
					/ (float)imageOriginal.getWidth(),
					(float)Math.min(wheelWidth, wheelHeight) 
					/ (float)imageOriginal.getHeight());
			imageScaled = Bitmap.createBitmap(imageOriginal, 0, 0,
					imageOriginal.getWidth(), imageOriginal.getHeight(),
					resize, false);
			// translate the matrix to the image view's center
			// 將矩陣的影象檢視的中心
			float translateX = wheelWidth / 2 - imageScaled.getWidth() / 2;
			float translateY = wheelHeight / 2 - imageScaled.getHeight() / 2;
			matrix.postTranslate(translateX, translateY);
			WheelMenu.this.setImageBitmap(imageScaled);
			WheelMenu.this.setImageMatrix(matrix);
		}
	}

	/**
	 * get the angle of a touch event. 得到一個觸控事件的角度。
	 */
	private double getAngle(double x, double y) {
		x = x - (wheelWidth / 2d);
		y = wheelHeight - y - (wheelHeight / 2d);

		switch (getQuadrant(x, y)) {
		case 1:
			return Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI;
		case 2:
			return 180 - Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI;
		case 3:
			return 180 + (-1 * Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
		case 4:
			return 360 + Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI;
		default:
			return 0;
		}
	}

	/**
	 * get the quadrant of the wheel which contains the touch point (x,y)
	 * 獲取包含觸控點的象限(X,Y輪)
	 * 
	 * @return quadrant 1,2,3 or 4
	 */
	private static int getQuadrant(double x, double y) {
		if (x >= 0) {
			return y >= 0 ? 1 : 4;
		} else {
			return y >= 0 ? 2 : 3;
		}
	}

	/**
	 * rotate the wheel by the given angle 轉動輪子由給定的角
	 * 
	 * @param degrees
	 *            旋轉的角度
	 * @param what 是否新增旋轉的總旋轉角度
	 */
	private void rotateWheel(float degrees, int what) {
		matrix.postRotate(degrees, wheelWidth / 2, wheelHeight / 2);
		WheelMenu.this.setImageMatrix(matrix);

		if(what == 0){
			// add the rotation to the total rotation
			// 新增旋轉的總旋轉角度
			totalRotation = totalRotation + degrees;
		}
	}
	
	/**
	 * 利用延時實現緩慢旋轉
	 * 
	 * @param degrees
	 */
	private void rotateDelayedWheel(double degrees){
		//儲存需要旋轉的總角度*
		leftoverRotation = degrees;
		
		//計算每次旋轉的角度
		everyTimeAngle = degrees / SEND_ROTATE_MSG_NUMBER;
		
		//傳送延時訊息
		mHandler.sendEmptyMessageDelayed(WHAT_ROTATE_WHEEL, ROTATE_INTERVAL_TIME);
	}
	
	/**處理旋轉訊息,handler的msg.what的值*/
	private final int WHAT_ROTATE_WHEEL = 0X000001;
	
	/**傳送handler訊息的時間間隔*/
	private final int ROTATE_INTERVAL_TIME = 20;
	
	/**傳送旋轉訊息的次數*/
	private final int SEND_ROTATE_MSG_NUMBER = 15;
	
	/**已經發送訊息的次數*/
	private int alreadySendNumber = 0;
	
	/**需要旋轉的總角度*/
	private double leftoverRotation = 0d;
	
	/**每次旋轉角度*/
	private double everyTimeAngle = 0d;
	
	/**
	 * 訊息處理
	 */
	private Handler mHandler = new Handler(){
		public void handleMessage(android.os.Message msg) {
			switch (msg.what) {
			//處理旋轉訊息
			case WHAT_ROTATE_WHEEL:
				//旋轉
				rotateWheel((float)everyTimeAngle, 1);
				//判斷,旋轉的次數是否達到指定指標
				if(++alreadySendNumber < SEND_ROTATE_MSG_NUMBER){
					//繼續延時傳送訊息
					mHandler.sendEmptyMessageDelayed(WHAT_ROTATE_WHEEL, ROTATE_INTERVAL_TIME);
				}else{
					//到達到次數限制
					cancelRotateParameter();
				}
				break;
			}
		}
	};
	
	/**重置rotate引數*/
	protected void cancelRotateParameter() {
		//將需要旋轉的總角度制空;
		leftoverRotation = 0d;
		//將每次旋轉的角度制空;
		everyTimeAngle = 0d;
		//將已經發送handler訊息的次數制空
		alreadySendNumber = 0;
	};
	
//	@Override
//	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//		
//		//加上下面的話即可實現listview在scrollview中滑動
//        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
//		
//		super.onMeasure(widthMeasureSpec, expandSpec);
//	}
	
	private double startAngle;
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		if(event.getY() > getHeight() && event.getX() > getWidth()){
			return false;
		}

		
		switch (event.getAction()) {

		case MotionEvent.ACTION_DOWN:
			// get the start angle for the current move event
			// 得到當前移動事件的起始角度
			startAngle = getAngle(event.getX(), event.getY());
			
			//將rotate緩慢旋轉操作停止
			mHandler.removeMessages(WHAT_ROTATE_WHEEL);
			
			//判斷,已經發送訊息的次數不等於0,表示進行了這種操作
			if(alreadySendNumber != 0){
				
				//重置旋轉的總角度,它的角度預設為車輪的頂點正中心,也就是正上方選中項的1/2角度
				totalRotation = divAngle / 2;
				
				//當車輪在緩慢的旋轉到頂點時,被打斷,進行第2,3,4,n次旋轉,方向為順時針或者逆時針,順時針旋轉角度為正數;逆時針旋轉角度為負數.有兩種結果
				//1.順時針旋轉,旋轉的角度為正;緩慢恢復到頂點時,它所執行的是逆時針旋轉.
						//如果車輪需要逆時針旋轉50度,回到頂點.我們在在它旋轉了25度之後打斷,進行下一次旋轉操作.
						//那麼它的頂點應該進行偏移,偏移的量為: 
						//預設的旋轉總角度 + 恢復中沒有走的角度(也就是: 需要恢復的總角度(50) - 已經走過的角度(25) = 沒有走過的角度(25)) = 偏移後的旋轉總角度.
						//所以,旋轉的總角度,應該是偏移後的;不然車輪的頂點座標會偏移,這個可是非常頭疼的.
				//2.逆時針旋轉,旋轉的角度為負;緩慢恢復到頂點時,它所執行的是順時針旋轉.
						//如果車輪需要順時針旋轉50度,回到頂點.我們在在它旋轉了25度之後打斷,進行下一次旋轉操作.
						//那麼它的頂點應該進行偏移,偏移的量為: 
						//預設的旋轉總角度 - 恢復中沒有走的角度(也就是: 需要恢復的總角度(50) - 已經走過的角度(25) = 沒有走過的角度(25)) = 偏移後的旋轉總角度.
						//所以,旋轉的總角度,應該是偏移後的;不然車輪的頂點座標會偏移,這個可是非常頭疼的.
				totalRotation = totalRotation - (leftoverRotation - alreadySendNumber * everyTimeAngle);
			}
			
			//重置執行rotate緩慢旋轉操作的引數
			cancelRotateParameter();
			
			break;

		case MotionEvent.ACTION_MOVE:
			// get the current angle for the current move event
			// 獲取當前的當前移動事件的角度
			double currentAngle = getAngle(event.getX(), event.getY());

			// rotate the wheel by the difference
			// 轉動輪子的角度
			rotateWheel((float) (startAngle - currentAngle), 0);

			// current angle becomes start angle for the next motion
			// 目前的角變為下次運動的起始角度
			startAngle = currentAngle;
			break;

		case MotionEvent.ACTION_UP:
			// get the total angle rotated in 360 degrees
			// 得到的總角度旋轉
			totalRotation = totalRotation % 360;

			// represent total rotation in positive value
			// 代表正面價值的總轉動
			if (totalRotation < 0) {
				totalRotation = 360 + totalRotation;
			}

			// calculate the no of divs the rotation has crossed
			// 計算旋轉的總的分
			int no_of_divs_crossed = (int) ((totalRotation) / divAngle);

			// calculate current top
			// 計算當前的頂部
			top = (divCount + top - no_of_divs_crossed) % divCount;

			// for next rotation, the initial total rotation will be the no
			// of degrees
			// inside the current top
			// 下次旋轉,初始總旋轉將沒有度,
			// 在當前最高
			totalRotation = totalRotation % divAngle;

			// snapping to the top's center
			// 捕捉到頂部的中心
			if (snapToCenterFlag) {

				// calculate the angle to be rotated to reach the top's
				// center.
				// 計算角度被旋轉到頂部的中心。
				double leftover = divAngle / 2 - totalRotation;

//				rotateWheel((float) (leftover), 1);
				rotateDelayedWheel(leftover);
				// re-initialize total rotation
				// 重新初始化總旋轉
				totalRotation = divAngle / 2;
			}

			// set the currently selected option
			// 將當前選定的選項
			if (top == 0) {
				selectedPosition = divCount - 1;// loop around the
												// array全陣列迴圈
			} else {
				selectedPosition = top - 1;
			}

			if (wheelChangeListener != null) {
				wheelChangeListener.onSelectionChange(selectedPosition);
			}

			break;
		}

		return true;
	}
	
//	@Override
//	public boolean onTouchEvent(MotionEvent event) {
//		return true;
//	}
//
//	
//	// listener for touch events on the wheel
//	// 觸控事件偵聽器的車輪
//	private class WheelTouchListener implements View.OnTouchListener {
//		
//
//		@Override
//		public boolean onTouch(View v, MotionEvent event) {
//			
//			return true;
//		}
//	}
	

	
	/**
	 * Interface to to observe user selection changes. 介面的使用者選擇的變化觀察。
	 */
	public interface WheelChangeListener {
		/**
		 * Called when user selects a new position in the wheel menu.
		 * 當用戶選擇一個新的位置在車輪選單
		 * 
		 * @param selectedPosition
		 *            the new position selected. 新的位置選擇
		 */
		public void onSelectionChange(int selectedPosition);
	}

}


2.MainActivity
package com.anupcowkur.wheelmenusample;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

import com.anupcowkur.wheelmenu.WheelMenu;

public class MainActivity extends Activity {

    private WheelMenu wheelMenu;
    private TextView selectedPositionText;
    private TextView contentText;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        wheelMenu = (WheelMenu) findViewById(R.id.wheelMenu);

        wheelMenu.setDivCount(3);
        wheelMenu.setWheelImage(R.drawable.vip_circle_2_bg);

        selectedPositionText = (TextView) findViewById(R.id.selected_position_text);
        selectedPositionText.setText("selected: " + (wheelMenu.getSelectedPosition() + 1));
        
        contentText = (TextView) findViewById(R.id.content_text);
        contentText.setText((wheelMenu.getSelectedPosition()+1)+"");

        wheelMenu.setWheelChangeListener(new WheelMenu.WheelChangeListener() {
            @Override
            public void onSelectionChange(int selectedPosition) {
                selectedPositionText.setText("selected: " + (selectedPosition + 1));
                contentText.setText((wheelMenu.getSelectedPosition()+1)+"");
            }
        });

    }
}

呵呵,註釋全在程式碼裡面,也有很多程式碼都是百度翻譯過來的,修改的部分才是自己寫的程式碼,呵呵

希望各位批評哦

追加..........

因為最後這個功能是需要整合到ScrollView中的,不用想也知道,它會與ScrollView的滑動事件衝突,所以在這裡給大家一個解決方案,可以解決這個問題....很頭疼的,試了很多方法.

直接上程式碼了

package com.hisun.sinldo.consult.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ScrollView;

public class PatientScrollView extends ScrollView {

	public PatientScrollView(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	
	public PatientScrollView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
	}

	public PatientScrollView(Context context) {
		super(context);
	}
	
	
	/**
	 * 重寫這個方法,解決與WheelMenu的觸控式螢幕事件衝突
	 */
	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		return false;
	}

}

自定義ScrollView,就只需要做一件事,就是重寫onInterceptTouchEvent(MotionEvent ev),然後,頭疼的事情就解決了,但是好像不知道原理哦

呵呵,看了看註釋,百度翻譯了一下.

Implement this method to intercept all touch screen motion events. This allows you to watch events as they are dispatched to your children, and take ownership of the current gesture at any point. 

實現這個方法來攔截所有的觸控式螢幕運動事件。這可以讓你看的事件,他們被派遣到你的孩子目前的所有權在任何點的手勢

說的意思好像是,攔截所有的觸控式螢幕運動事件,但是自己不處理而是交給孩子;如果孩子沒有處理的,最後在自己來收收尾.......

嘿嘿