自定义圆形菜单Demo

</pre><p></p><p><pre name="code" class="java">package com.example.mycirclemenulayoutdemo.views.circlemenulayout;

import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;

import com.example.mycirclemenulayoutdemo.R;

public class CircleMenuLayout extends ViewGroup {

	private int mRadius;
	/**
	 * 该容器内child item的默认尺寸
	 */
	private static final float RADIO_DEFAULT_CHILD_DIMENSION = 1 / 4f;
	/**
	 * 菜单的中心child的默认尺寸
	 */
	private float RADIO_DEFAULT_CENTERITEM_DIMENSION = 1 / 3f;

	/**
	 * 该容器的内边距,无视padding属性,如需边距请用该变量
	 */
	private float mPadding;
	/**
	 * 该容器的内边距,无视padding属性,如需边距请用该变量
	 */
	private static final float RADIO_PADDING_LAYOUT = 1 / 12f;

	/**
	 * 布局时的开始角度
	 */
	private double mStartAngle = 0;

	/**
	 * 当每秒移动角度达到该值时,认为是快速移动
	 */
	private static final int FLINGABLE_VALUE = 300;

	/**
	 * 检测按下到抬起时旋转的角度
	 */
	private float mTmpAngle;

	/**
	 * 当每秒移动角度达到该值时,认为是快速移动
	 */
	private int mFlingableValue = FLINGABLE_VALUE;

	public CircleMenuLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
		// 无视padding
		setPadding(0, 0, 0, 0);
	}

	/** 设置menu item 的位置 */
	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		int layoutRadius = mRadius;

		// 放置子View的位置
		final int childCount = getChildCount();

		int left, top;

		// menu item 的尺寸
		int cWidth = (int) (layoutRadius * RADIO_DEFAULT_CHILD_DIMENSION);

		// 根据 menu item 的个数,计算角度
		float angleDelay = 360 / (getChildCount() - 1);

		// 遍历去设置menuitem 的位置
		for (int i = 0; i < childCount; i++) {
			final View child = getChildAt(i);

			if (child.getId() == R.id.id_circle_menu_item_center)
				continue;
			if (child.getVisibility() == GONE) {
				continue;
			}
			mStartAngle %= 360;

			// 计算 中心点到menu item中心点的距离
			float tmp = layoutRadius / 2f - cWidth / 2 - mPadding;

			// tmp cosa 即为menu item 中心点的横坐标
			left = layoutRadius
					/ 2
					+ (int) Math.round(tmp
							* Math.cos(Math.toRadians(mStartAngle)) - 1 / 2f
							* cWidth);
			// tmp sina 即menu item 的纵坐标
			top = layoutRadius
					/ 2
					+ (int) Math.round(tmp
							* Math.sin(Math.toRadians(mStartAngle)) - 1 / 2f
							* cWidth);

			child.layout(left, top, left + cWidth, top + cWidth);

			// 叠加尺寸
			mStartAngle += angleDelay;

		}

		View cView = findViewById(R.id.id_circle_menu_item_center);
		if (cView != null) {
			cView.setOnClickListener(new OnClickListener() {

				@Override
				public void onClick(View v) {
					// TODO Auto-generated method stub
					if (mOnMenuItemClickListener != null) {
						mOnMenuItemClickListener.itemCenterClick(v);
					}
				}
			});

			// 设置Center item位置
			int c1 = layoutRadius / 2 - cView.getMeasuredWidth() / 2;
			int cr = c1 + cView.getMeasuredWidth();
			cView.layout(c1, c1, cr, cr);
		}
		Log.e("onLayout", "------------onLayout");

	}

	/**
	 * 设置布局的宽高,并策略menu item的宽高
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		/** 资源的高度和宽度 */
		int resWidth = 0;
		int resHeight = 0;

		/*
		 * 最简单的映射关系是
		 * 
		 * wrap_parent -> MeasureSpec.AT_MOST
		 * 
		 * match_parent -> MeasureSpec.EXACTLY
		 * 
		 * 具体值 -> MeasureSpec.EXACTLY
		 * 
		 * RelativeLayout 是一个比较复杂的 ViewGroup,其中子 view 的大小不仅跟
		 * layout_width、layout_height 属性相关,还更很多属性有关系,所以会改变上述映射情况,使结果变得特别复杂。
		 */

		/*
		 * 一个MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求。
		 * 
		 * 一个MeasureSpec由大小和模式组成。
		 * 
		 * 它有三种模式:UNSPECIFIED(未指定),父元素不对自元素施加任何束缚,子元素可以得到任意想要的大小;
		 * 
		 * EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;
		 * 
		 * AT_MOST(至多),子元素至多达到指定大小的值。
		 * 
		 * 它常用的三个函数:
		 * 
		 * 1.static int getMode(int measureSpec):根据提供的测量值(格式)提取模式(上述三个模式之一)
		 * 
		 * 2.static int getSize(int
		 * measureSpec):根据提供的测量值(格式)提取大小值(这个大小也就是我们通常所说的大小)
		 * 
		 * 3.static int makeMeasureSpec(int size,int
		 * mode):根据提供的大小值和模式创建一个测量值(格式)
		 * 
		 * 这个类的使用呢,通常在view组件的onMeasure方法里面调用.
		 */
		/** 根据传入的参数,分别获取测量模式和测量值 */
		int width = MeasureSpec.getSize(widthMeasureSpec);
		int widthMode = MeasureSpec.getMode(widthMeasureSpec);
		int height = MeasureSpec.getSize(heightMeasureSpec);
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);

		/** 如果宽或者高的测量值模式非精确值 */
		if (widthMode != MeasureSpec.EXACTLY
				|| heightMode != MeasureSpec.EXACTLY) {
			// 主要设置为背景图的高度
			resWidth = getSuggestedMinimumWidth();
			// 如果没有设置背景图片,则设置为屏幕宽高的默认值
			resWidth = resWidth == 0 ? getDefaultWidth() : resWidth;

			// 设置背景图的高度
			resHeight = getSuggestedMinimumHeight();
			resHeight = resHeight == 0 ? getDefaultWidth() : resHeight;
		} else {
			// 如果都设置精确值,就取最小值
			resWidth = resHeight = Math.min(width, height);
		}
		setMeasuredDimension(resWidth, resHeight);

		// 获得直径
		mRadius = Math.max(getMeasuredWidth(), getMeasuredHeight());

		// menu item 的数量
		final int count = getChildCount();
		// menu item 的尺寸和测量模式
		int childSize = (int) (mRadius * RADIO_DEFAULT_CHILD_DIMENSION);
		int childMode = MeasureSpec.EXACTLY;
		// 迭代测量
		for (int i = 0; i < count; i++) {
			final View child = getChildAt(i);
			if (child.getVisibility() == View.GONE) {
				continue;
			}
			// 计算menu item 的尺寸;和设置好的模式,去对item进行测量
			int makeMeasureSpec = -1;

			if (child.getId() == R.id.id_circle_menu_item_center) {
				makeMeasureSpec = MeasureSpec.makeMeasureSpec(
						(int) (mRadius * RADIO_DEFAULT_CENTERITEM_DIMENSION),
						childMode);
			} else {
				makeMeasureSpec = MeasureSpec.makeMeasureSpec(childSize,
						childMode);
			}
			// 子View定尺寸
			child.measure(makeMeasureSpec, makeMeasureSpec);
		}
		mPadding = RADIO_PADDING_LAYOUT * mRadius;
		Log.e("onMeasure", "--------onMeasure");
	}

	/**
	 * 获得默认该layout的尺寸
	 */
	private int getDefaultWidth() {
		/*
		 * DisplayMetrics可以得到分辨率等信息
		 * 
		 * metrics.widthPixels 屏幕宽
		 * 
		 * metrics.heightPixels 屏幕高
		 * 
		 * metrics.density 屏幕密度
		 * 
		 * pixels = dps * (density / 160)
		 * 
		 * density与metrics.density不是一个东西,它们的关系是:metrics.density = density / 160
		 * 
		 * 自android1.6及以后,DisplayMetrics得到的宽和高都是以px为单位的,不需要转换。
		 */
		WindowManager wm = (WindowManager) getContext().getSystemService(
				Context.WINDOW_SERVICE);
		DisplayMetrics outMetrics = new DisplayMetrics();
		wm.getDefaultDisplay().getMetrics(outMetrics);
		return Math.min(outMetrics.widthPixels, outMetrics.heightPixels);
	}

	/**
	 * MenuItem的点击事件接口
	 * 
	 */
	public interface OnMenuItemClickListener {
		void itemClick(View view, int pos);

		void itemCenterClick(View view);
	}

	/**
	 * MenuItem的点击事件接口
	 */
	private OnMenuItemClickListener mOnMenuItemClickListener;

	/**
	 * 设置MenuItem的点击事件接口
	 * 
	 * @param mOnMenuItemClickListener
	 */
	public void setOnMenuItemClickListener(
			OnMenuItemClickListener mOnMenuItemClickListener) {
		this.mOnMenuItemClickListener = mOnMenuItemClickListener;
	}

	/**
	 * 菜单项的文本
	 */
	private String[] mItemTexts;
	/**
	 * 菜单项的图标
	 */
	private int[] mItemImgs;
	/**
	 * 菜单的个数
	 */
	private int mMenuItemCount;

	/**
	 * 设置菜单条目的图标和文本
	 * 
	 * @param resIds
	 */
	public void setMenuItemIconsAndTexts(int[] resIds, String[] texts) {
		mItemImgs = resIds;
		mItemTexts = texts;
		// 参数检查
		if (resIds == null && texts == null) {
			throw new IllegalArgumentException("菜单项文本和图片至少设置一项");
		}

		// 初始化 mMenuCount
		mMenuItemCount = resIds == null ? texts.length : resIds.length;

		if (resIds != null && texts != null) {
			mMenuItemCount = Math.min(resIds.length, texts.length);
		}

		addMenuItems();
	}

	private int mMenuItemLayoutId = R.layout.circle_menu_item;

	/**
	 * 添加菜单项
	 */
	private void addMenuItems() {
		LayoutInflater inflater = LayoutInflater.from(getContext());
		/** 根据用户设置的参数,初始化view */
		for (int i = 0; i < mMenuItemCount; i++) {
			final int j = i;
			View view = inflater.inflate(mMenuItemLayoutId, this, false);

			ImageView iv = (ImageView) view
					.findViewById(R.id.id_circle_menu_item_image);
			TextView tv = (TextView) view
					.findViewById(R.id.id_circle_menu_item_text);

			if (iv != null) {
				iv.setVisibility(View.VISIBLE);
				iv.setImageResource(mItemImgs[i]);
				iv.setOnClickListener(new OnClickListener() {

					@Override
					public void onClick(View v) {
						// TODO Auto-generated method stub
						if (mOnMenuItemClickListener != null) {
							mOnMenuItemClickListener.itemClick(v, j);
						}
					}
				});
			}

			if (tv != null) {
				tv.setVisibility(View.VISIBLE);
				tv.setText(mItemTexts[i]);
			}

			// 添加view
			addView(view);
		}
	}

	/**
	 * 记录上一次的x,y坐标
	 */
	private float mLastX;
	private float mLastY;

	/**
	 * 自动滚动的Runnable
	 */
	private AutoFlingRunnable mFlingRunnable;
	/**
	 * 检测按下到抬起时使用的时间
	 */
	private long mDownTime;

	/**
	 * 判断是否正在自动滚动
	 */
	private boolean isFling;

	/**
	 * 如果移动角度达到该值,则屏蔽点击
	 */
	private static final int NOCLICK_VALUE = 3;

	/**
	 * 自动滚动的任务
	 * 
	 * 
	 */
	private class AutoFlingRunnable implements Runnable {

		private float angelPerSencond;

		public AutoFlingRunnable(float velocity) {
			this.angelPerSencond = velocity;
		}

		@Override
		public void run() {
			if ((int) Math.abs(angelPerSencond) < 20) {
				isFling = false;
				return;
			}
			isFling = true;

			// 不断改变mStartAngle,让其滚动, /30为了避免滚动太快
			mStartAngle += (angelPerSencond / 30);

			// 逐渐减少这个值
			angelPerSencond /= 1.0666F;
			postDelayed(this, 30);
			// 重新布局
			requestLayout();
		}

	}

	/**
	 * 如果每秒旋转角度到达该值,则认为是自动滚动
	 * 
	 * @param mFlingableValue
	 */
	public void setFlingableValue(int mFlingableValue) {
		this.mFlingableValue = mFlingableValue;
	}

	/**
	 * 设置内边距的比例
	 * 
	 * @param mPadding
	 */
	public void setPadding(float mPadding) {
		this.mPadding = mPadding;
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {

		float x = event.getX();
		float y = event.getY();
		Log.e("dispatchDragEvent", "-------------" + event.getAction() + "");
		Log.e("dispatchDragEvent", "------------- x: " + x + "");
		Log.e("dispatchDragEvent", "------------- y: " + y + "");

		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			mLastX = x;
			mLastY = y;
			mDownTime = System.currentTimeMillis();
			mTmpAngle = 0;

			// 如果当前已经在快速滚动
			if (isFling) {
				// 移除快速滚动的回调
				removeCallbacks(mFlingRunnable);
				isFling = false;
				return true;
			}
			break;
		case MotionEvent.ACTION_MOVE:
			/**
			 * 获得开始的角度
			 */
			float start = getAngle(mLastX, mLastY);
			/**
			 * 获得当前的角度
			 */
			float end = getAngle(x, y);

			// 如果是一、四象限,则直接end - start,角度值都是正值
			if (getQuadrant(x, y) == 1 || getQuadrant(x, y) == 4) {
				mStartAngle += end - start;
				mTmpAngle += end - start;

			} else// 二、三象限,设角度值是负值
			{
				mStartAngle += start - end;
				mTmpAngle += start - end;
			}
			// 重新布局
			requestLayout();
			mLastX = x;
			mLastY = y;

		case MotionEvent.ACTION_UP:

			// 计算,每秒移动的角度
			float anglePerSecond = mTmpAngle * 1000
					/ (System.currentTimeMillis() - mDownTime);

			// Log.e("TAG", anglePrMillionSecond + " , mTmpAngel = " +
			// mTmpAngle);

			// 如果达到该值认为是快速移动
			if (Math.abs(anglePerSecond) > mFlingableValue && !isFling) {
				// post一个任务,去自动滚动
				post(mFlingRunnable = new AutoFlingRunnable(anglePerSecond));

				return true;
			}

			// 如果当前旋转角度超过NOCLICK_VALUE屏蔽点击
			if (Math.abs(mTmpAngle) > NOCLICK_VALUE) {
				return true;
			}

			break;
		}
		return super.dispatchTouchEvent(event);
	}

	/**
	 * 根据当前位置计算象限
	 * 
	 * @param x
	 * @param y
	 * @return
	 */
	private int getQuadrant(float x, float y) {
		int tmpX = (int) (x - mRadius / 2);
		int tmpY = (int) (y - mRadius / 2);
		if (tmpX >= 0) {
			return tmpY >= 0 ? 4 : 1;
		} else {
			return tmpY >= 0 ? 3 : 2;
		}
	}

	/**
	 * 根据触摸的位置,计算角度
	 * 
	 * @param xTouch
	 * @param yTouch
	 * @return
	 */
	private float getAngle(float xTouch, float yTouch) {
		double x = xTouch - (mRadius / 2d);
		double y = yTouch - (mRadius / 2d);
		return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);// Math.hypot对于给定的直角三角形的两个直角边,求其斜边的长度。
	}
}




yuo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值