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. 

实现这个方法来拦截所有的触摸屏运动事件。这可以让你看的事件,他们被派遣到你的孩子目前的所有权在任何点的手势

说的意思好像是,拦截所有的触摸屏运动事件,但是自己不处理而是交给孩子;如果孩子没有处理的,最后在自己来收收尾.......

嘿嘿


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值