一、前言
旋转菜单是一种占用空间较大,实用性稍弱的UI,一方面由于展示空间的问题,其展示的数据有限,但另一方面真由于这个原因,对用户而言趣味性和操作性反而更有好。
二、绘制原理
绘制原理很简单,通过细微的观察,我们发现文字是不需要旋转的,也就是每个菜单是不需要自旋转,只需要旋转其位置坐标即可,实际上其难点并不是绘制,而是在于触摸事件的处理方式。
本篇菜单特性:
- 动态设置菜单
- 计算旋转方向和旋转角度
- 支持点击
难点1:
旋转方向判断,旋转时记录起始点,计算出旋转方向。
首先,我们要理解,Touch事件也存在抽象的坐标体系,和View左上角重合,因此我们需要转换坐标
float cx = event.getX() - getWidth() / 2F; float cy = event.getY() - getHeight() / 2F;
旋转角度的计算
这种计算是为了计算出与原始落点位置的夹角,这里的方法是计算使用Math.asin反正切函数,然后结合坐标系进行判断
float lineWidth = (float) Math.sqrt(Math.pow(cx, 2) + Math.pow(cy, 2)); float degreeRadian = (float) Math.asin(cy / lineWidth); float dr = 0; if (cy > 0) { //一二象限 if (cx > 0) { dr = degreeRadian; } else { dr = (float) ((Math.PI - degreeRadian)); } } else { //三四象限 if (cx > 0) { dr = (float) (Math.PI * 2 - Math.abs(degreeRadian)); } else { dr = (float) ((Math.PI + Math.abs(degreeRadian))); } }
由于对Math的了解我们知道,Math.asin不能反映真实的夹角,因此需要做上面的补充。但是后来我们发现,Math.atan2函数的存在,直接可以求出斜率夹角,而且不会丢失象限关系,一下子就省了好几行代码。
dr = (float) Math.atan2(cy, cx);
难点2:实时更新
为了旋转,我们可能忘记记录最新位置,这个可能导致圆反向旋转,因此要实时记录位置
eStartX = cx; eStartY = cy;
难点3:由于拦截了UP事件,因此需要对UP事件进行专门处理
if (System.currentTimeMillis() - startDownTime > 500) { break; } float upX = event.getX() - getWidth() / 2F; float upY = event.getY() - getHeight() / 2F; handleClickTap(upX, upY);
全部代码:
public class OribitView extends View { private final String TAG = "OribitView"; private DisplayMetrics displayMetrics; private float mOutlineRaduis; private float mInlineRadius; private TextPaint mPaint; private float lineWidth = 5f; private float textSize = 12f; private int itemCount = 5; private int mTouchSlop = 0; private float rotateDegreeRadian = 0; private OnItemClickListener onItemClickListener; private float eStartX = 0f; private float eStartY = 0f; private boolean isMoveTouch = false; private float startDegreeRadian = 0l; //记录用于落点角度,用于参考 private long startDownTime = 0l; Rect bounds = new Rect(); private final List<OribitItemPoint> mOribitItemPoints = new ArrayList<>(); public OribitView(Context context) { this(context, null); } public OribitView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public OribitView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); displayMetrics = context.getResources().getDisplayMetrics(); initPaint(); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); setLayerType(LAYER_TYPE_SOFTWARE,null); } private void initPaint() { // 实例化画笔并打开抗锯齿 mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); mPaint.setAntiAlias(true); mPaint.setTextSize(dpToPx(textSize)); } private float dpToPx(float dp) { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); if (widthMode != MeasureSpec.EXACTLY) { widthSize = displayMetrics.widthPixels / 2; } int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY) { heightSize = displayMetrics.widthPixels / 2; } widthSize = heightSize = Math.min(widthSize, heightSize); setMeasuredDimension(widthSize, heightSize); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mOutlineRaduis = w / 2.0f - dpToPx(lineWidth); mInlineRadius = mOutlineRaduis * 3 / 5.0f - dpToPx(lineWidth); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int width = getWidth(); int height = getWidth(); mPaint.setStyle(Paint.Style.STROKE); mPaint.setStrokeWidth(dpToPx(lineWidth / 4)); mPaint.setColor(Color.GRAY); int id = canvas.save(); float centerRadius = (mOutlineRaduis + mInlineRadius) / 2; float itemRadius = (mOutlineRaduis - mInlineRadius) / 2; canvas.translate(width / 2F, height / 2F); // canvas.drawCircle(0, 0, mOutlineRaduis, mPaint); //画外框 // canvas.drawCircle(0, 0, mInlineRadius, mPaint); //画内框 float strokeWidth = mPaint.getStrokeWidth(); mPaint.setStrokeWidth(itemRadius * 2 - dpToPx(lineWidth / 2)); mPaint.setColor(Color.DKGRAY); mPaint.setShadowLayer(10,0,10,Color.DKGRAY); canvas.drawCircle(0, 0, centerRadius, mPaint); mPaint.setStrokeWidth(strokeWidth); float degree = (float) (2 * Math.asin(itemRadius / centerRadius)); //计算出从原点过item的切线夹角,求出每个圆所占夹角大小 float spaceDegree = (float) ((Math.PI * 2 - degree * itemCount) / itemCount); for (int i = 0; i < mOribitItemPoints.size(); i++) { OribitItemPoint itemPoint = mOribitItemPoints.get(i); float x = (float) (centerRadius * Math.cos(rotateDegreeRadian + i * (spaceDegree + degree))); float y = (float) (centerRadius * Math.sin(rotateDegreeRadian + i * (spaceDegree + degree))); itemPoint.x = x; itemPoint.y = y; OribitItem oribitItem = itemPoint.getOribitItem(); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(oribitItem.backgroundColor); //减去线宽 float strokeOffset = dpToPx(lineWidth / 2); canvas.drawCircle(x, y, itemRadius - strokeOffset, mPaint); mPaint.setColor(oribitItem.textColor); String text = String.valueOf(oribitItem.text); mPaint.getTextBounds(text, 0, text.length(), bounds); float textBaseline = getTextPaintBaseline(mPaint) - y - bounds.height() + strokeOffset; canvas.drawText(text, x - bounds.width() / 2F, -textBaseline, mPaint); } canvas.restoreToCount(id); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: eStartX = event.getX() - getWidth() / 2F; //这里转为原点为画布中心的点,便于计算角度 eStartY = event.getY() - getHeight() / 2F; //求出落点与坐标系x轴方向的夹角( float locationRadian = (float) Math.asin(eStartY / (float) Math.sqrt(Math.pow(eStartX, 2) + Math.pow(eStartY, 2))); // //根据正弦值计算起点在那个象限 // if (eStartY > 0) { // //一二象限 // if (eStartX < 0) { // startDegreeRadian = (float) (Math.PI - locationRadian); // } else { // startDegreeRadian = locationRadian; // } // } else { // //三四象限 // if (eStartX > 0) { // startDegreeRadian = (float) (Math.PI * 2 - Math.abs(locationRadian)); // } else { // startDegreeRadian = (float) (Math.PI + Math.abs(locationRadian)); // } // } startDegreeRadian = locationRadian; startDownTime = System.currentTimeMillis(); getParent().requestDisallowInterceptTouchEvent(true); super.onTouchEvent(event); return true; case MotionEvent.ACTION_MOVE: //坐标转换 float cx = event.getX() - getWidth() / 2F; float cy = event.getY() - getHeight() / 2F; float dx = cx - eStartX; float dy = cy - eStartY; float slideSlop = (float) Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); if (slideSlop > mTouchSlop) { isMoveTouch = true; } else { isMoveTouch = false; } if (isMoveTouch) { float lineWidth = (float) Math.sqrt(Math.pow(cx, 2) + Math.pow(cy, 2)); float degreeRadian = (float) Math.asin(cy / lineWidth); float dr = 0; // // if (cy > 0) { // //一二象限 // if (cx > 0) { // dr = degreeRadian; // } else { // dr = (float) ((Math.PI - degreeRadian)); // } // // } else { // //三四象限 // if (cx > 0) { // dr = (float) (Math.PI * 2 - Math.abs(degreeRadian)); // } else { // dr = (float) ((Math.PI + Math.abs(degreeRadian))); // } // } dr = (float) Math.atan2(cy, cx); rotateDegreeRadian += (dr - startDegreeRadian); startDegreeRadian = dr; eStartX = cx; eStartY = cy; postInvalidate(); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_OUTSIDE: getParent().requestDisallowInterceptTouchEvent(false); if (isMoveTouch) { isMoveTouch = false; break; } if (System.currentTimeMillis() - startDownTime > 500) { break; } float upX = event.getX() - getWidth() / 2F; float upY = event.getY() - getHeight() / 2F; handleClickTap(upX, upY); break; } return super.onTouchEvent(event); } private void handleClickTap(float upX, float upY) { if (itemCount == 0 || mOribitItemPoints == null) return; OribitItemPoint clickItemPoint = null; float itemRadius = (mOutlineRaduis - mInlineRadius) / 2; for (OribitItemPoint itemPoint : mOribitItemPoints) { if (Float.isNaN(itemPoint.x) || Float.isNaN(itemPoint.y)) { continue; } float dx = (itemPoint.x - upX); float dy = (itemPoint.y - upY); float clickSlop = (float) Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); if (clickSlop >= itemRadius) { continue; } clickItemPoint = itemPoint; break; } if (clickItemPoint == null) return; if (this.mOribitItemPoints != null) { this.onItemClickListener.onItemClick(this, clickItemPoint.oribitItem); } } public int getItemCount() { return itemCount; } public static float getTextPaintBaseline(Paint p) { Paint.FontMetrics fontMetrics = p.getFontMetrics(); return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent; } public void showItems(List<OribitItem> oribitItems) { mOribitItemPoints.clear(); if (oribitItems != null) { for (OribitItem item : oribitItems) { OribitItemPoint point = new OribitItemPoint(); point.x = Float.NaN; point.y = Float.NaN; point.oribitItem = item; mOribitItemPoints.add(point); } } this.itemCount = mOribitItemPoints.size(); postInvalidate(); } public void setOnItemClickListener(OnItemClickListener onItemClickListener) { this.onItemClickListener = onItemClickListener; } public static class OribitItem { public String text; public int textColor; public int backgroundColor; } static class OribitItemPoint<T extends OribitItem> extends PointF { private T oribitItem; public void setOribitItem(T oribitItem) { this.oribitItem = oribitItem; } public T getOribitItem() { return oribitItem; } } public interface OnItemClickListener { public void onItemClick(View contentView, OribitItem item); } }
用法:
OribitView oribitView = findViewById(R.id.oribitView); oribitView.setOnItemClickListener(new OribitView.OnItemClickListener() { @Override public void onItemClick(View contentView, OribitView.OribitItem item) { Toast.makeText(contentView.getContext(),item.text,Toast.LENGTH_SHORT).show(); } }); List<OribitView.OribitItem> oribitItems = new ArrayList<>(); String[] chs = new String[]{"鲜花", "牛奶", "橘子", "生活", "新闻", "热点"}; int[] colors = new int[]{argb(random.nextFloat(), random.nextFloat(), random.nextFloat()), argb(random.nextFloat(), random.nextFloat(), random.nextFloat()), argb(random.nextFloat(), random.nextFloat(), random.nextFloat()), argb(random.nextFloat(), random.nextFloat(), random.nextFloat()), argb(random.nextFloat(), random.nextFloat(), random.nextFloat()), argb(random.nextFloat(), random.nextFloat(), random.nextFloat()) }; for (int i = 0; i < chs.length; i++) { OribitView.OribitItem item = new OribitView.OribitItem(); item.text = chs[i]; item.textColor = Color.WHITE; item.backgroundColor = colors[i]; oribitItems.add(item); } oribitView.showItems(oribitItems);
三、总结
本篇难点主要是事件处理,当然可能有人会问,使用Layout添加岂不是更方便,答案是肯定的,但是本篇主要重点介绍Canvas 绘制,后续有Layout的布局,当然这里其实区别并不大,不同点是一个需要onLayout的调用,另一个是onDraw的调用,做好坐标轴转换即可,难度并不大。
以上就是Android自定义实现转盘菜单的详细内容,更多关于Android转盘菜单的资料请关注其它相关文章!