自定义圆形菜单Demo

来源:互联网 发布:棒球帽 知乎 编辑:程序博客网 时间:2024/05/16 16:20
</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);// 无视paddingsetPadding(0, 0, 0, 0);}/** 设置menu item 的位置 */@Overrideprotected 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() {@Overridepublic void onClick(View v) {// TODO Auto-generated method stubif (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的宽高 */@Overrideprotected 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("菜单项文本和图片至少设置一项");}// 初始化 mMenuCountmMenuItemCount = 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() {@Overridepublic void onClick(View v) {// TODO Auto-generated method stubif (mOnMenuItemClickListener != null) {mOnMenuItemClickListener.itemClick(v, j);}}});}if (tv != null) {tv.setVisibility(View.VISIBLE);tv.setText(mItemTexts[i]);}// 添加viewaddView(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;}@Overridepublic 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;}@Overridepublic 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

0 0
原创粉丝点击