Android 自定义转盘菜单OribitView

原创
2020/07/18 09:06
阅读数 511

一、需求分析

产品角度:工作中往往存在很多特殊需求,转盘轨道菜单就是其中一种,比如汽车内置显示屏呼出菜单,比如电视机菜单。

技术角度:通过数学三角函数+Canvas Api实现,数学知识非常重要,以往的自定义View,很多都是通过数学三角函数、向量来实现的,本例也不例外。

 

效果预览

代码实现

public class OribitView extends View {

    private final String TAG = "OribitView";
    private  DisplayMetrics displayMetrics;
    private float mOutlineRaduis;
    private float mInlineRadius;
    private TextPaint mPaint;
    private int lineWidth  = 5;
    private int textSize = 18;
    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;


    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();

    }

    private void initPaint() {
        // 实例化画笔并打开抗锯齿
        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG );
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(dpTopx(textSize));

    }
    private float dpTopx(int 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/2));
        mPaint.setColor(Color.GRAY);
        int id = canvas.save();

        float centerRadius =   (mOutlineRaduis+mInlineRadius)/2;
        float itemRadius =  (mOutlineRaduis - mInlineRadius)/2;

        canvas.translate(width/2,height/2);


        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.LTGRAY);
        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);

        mPaint.setColor(Color.RED);


        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.STROKE);
            mPaint.setColor(Color.DKGRAY);
            canvas.drawCircle(x,y,itemRadius-dpTopx(lineWidth/2),mPaint);
            mPaint.setStyle(Paint.Style.FILL);

            mPaint.setColor(oribitItem.backgroundColor);
            canvas.drawCircle(x,y,itemRadius-dpTopx(lineWidth/2),mPaint);

            mPaint.setColor(oribitItem.textColor);
            String text = String.valueOf(oribitItem.text);
            Rect bounds = new Rect();
            mPaint.getTextBounds(text, 0, text.length(), bounds);
            float textBaseline =    getTextPaintBaseline(mPaint) - y- bounds.height();
            canvas.drawText(text,x-bounds.width()/2,-textBaseline,mPaint);


        }
        canvas.restoreToCount(id);

    }



    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()){
            case  MotionEvent.ACTION_DOWN:
                    eStartX = event.getX()-getWidth()/2;
                    //这里转为原点为画布中心的点,便于计算角度
                    eStartY = event.getY()-getHeight()/2;

                    //求出落点与坐标系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));
                        }
                    }
                    startDownTime  = System.currentTimeMillis();
                    getParent().requestDisallowInterceptTouchEvent(true);
                    super.onTouchEvent(event);
                    return true;
            case  MotionEvent.ACTION_MOVE:
                    float cx = event.getX() - getWidth()/2;
                    float cy =  event.getY() - getHeight()/2;

                    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)));
                            }

                        }
                        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()/2;
                float upY =  event.getY() - getHeight()/2;
                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) return;
        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[]{0xffc4e1ff,0xffff7575,0xff6fb7b7,0xffff9922,0xffb766dd,0xff28ff28};
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);

 

展开阅读全文
加载中

作者的其它热门文章

打赏
1
0 收藏
分享
打赏
0 评论
0 收藏
1
分享
返回顶部
顶部