一、前语

旋转菜单是一种占用空间较大,实用性稍弱的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的调用,做好坐标轴转化即可,难度并不大。