欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

仿遥控器菜单圆盘

程序员文章站 2022-07-04 13:34:32
...

仿遥控器菜单圆盘

1.效果

仿遥控器菜单圆盘


2.github地址

github地址
https://github.com/caixingcun/CopyMenu

3.前言

之前有一家面试,有这么一个需求,问我需要如何实现这么一个效果
刚开始感觉相对布局什么也能实现差不多的,但面试官提问到如何处理重叠部分
之后回答说自定义控件,因为自定义控件写的少,也就只有个大概思路
一带而过了,回来花了点时间,搞出来,跟大家分享下,也是来扩充博客

4.原理

这样的需要主要麻烦的地方是在点击后,响应特定区块
如果只是单纯画图 和 点击对应区域回调,完全可以画同心圆 和 线来实现
这里是将每一个区块的线上的点计算出来 连成path
创建特定标志位记录点击区域,重绘区块实现区块变色

5.代码

step1.创建一个类继承View,创建构造函数,在构造中初始化自定义属性 和 画笔

public class MenuView extends View 
 public MenuView(Context context) {
        this(context, null);
    }

    public MenuView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MenuView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性。
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.menuview);
        //获取字体大小,默认大小是16dp
        mLineColor = ta.getColor(R.styleable.menuview_lineColor, Color.BLACK);
        mArrowColor = ta.getColor(R.styleable.menuview_arrowColor, Color.BLACK);
        mChoseColor = ta.getColor(R.styleable.menuview_choseColor, Color.BLACK);
        mArrowChoseColor = ta.getColor(R.styleable.menuview_arrowChoseColor, Color.WHITE);
        mText = (String) ta.getText(R.styleable.menuview_mtext);
        mTextSize = ta.getDimension(R.styleable.menuview_mtextSize, 30);
        Log.d("tag", "mTextSize" + mTextSize);
        ta.recycle();

        mPaint = new Paint();
        mPaint.setColor(mLineColor);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(2);//线宽
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize(mTextSize);
        mPaint.setTypeface(Typeface.DEFAULT_BOLD);
    }

step2.自定义属性 ,声明命名空间

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="menuview">
        <!--线条颜色-->
        <attr name="lineColor" format="color"/>
        <!--箭头颜色-->
        <attr name="arrowColor" format="color"/>
        <!--选择反色-->
        <attr name="choseColor" format="color"/>
        <!--选择箭头反色-->
        <attr name="arrowChoseColor" format="color"/>
        <!--中间OK或者菜单字体大小-->
        <attr name="mtextSize" format="dimension"/>
        <!--中间文字内容-->
        <attr name="mtext" format="string"/>

    </declare-styleable>
</resources>

step3.重写onMeasure方法回去控件宽高

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d("tag", "onMeasure");
        //1920x1080分辨率
        mWidth = getMeasuredWidth();  //获取控件宽度和高度
        mHeight = getMeasuredHeight();//600,600

step4.重写onDraw方法进行绘制界面

 private PointF mCenterPoint = new PointF(200f, 200f);
    private float mR = mWidth * 0.5f;
    private float mr = mR * 75f / 200f;
    private float gen2 = 1.41421356f;
    private PointF mPointA = new PointF(mCenterPoint.x - mR / gen2, mCenterPoint.y - mR / gen2);
    private PointF mPointB = new PointF(mCenterPoint.x + mR / gen2, mCenterPoint.y - mR / gen2);
    private PointF mPointC = new PointF(mCenterPoint.x + mr / gen2, mCenterPoint.y - mr / gen2);
    private PointF mPointD = new PointF(mCenterPoint.x - mr / gen2, mCenterPoint.y - mr / gen2);

    private PointF mPointE = new PointF(mCenterPoint.x - mR / gen2, mCenterPoint.y + mR / gen2);
    private PointF mPointF = new PointF(mCenterPoint.x + mR / gen2, mCenterPoint.y + mR / gen2);
    private PointF mPointG = new PointF(mCenterPoint.x + mr / gen2, mCenterPoint.y + mr / gen2);
    private PointF mPointH = new PointF(mCenterPoint.x - mr / gen2, mCenterPoint.y + mr / gen2);

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d("tag", "onDraw");
       /** 圆心*/
        mCenterPoint = new PointF(mWidth * 0.5f,
                mHeight * 0.5f);
        Log.d("tag", mCenterPoint.x + "mCenterPoint.x" + mCenterPoint.y + "mCenterPoint.y");
        /**大小半径*/
        mR = mWidth * 0.5f;
        mr = mR * 75f / 200f;
        /** 四个圆弧区域的路径相关点*/
        mPointA = new PointF(mCenterPoint.x - mR / gen2, mCenterPoint.y - mR / gen2);
        mPointB = new PointF(mCenterPoint.x + mR / gen2, mCenterPoint.y - mR / gen2);
        mPointC = new PointF(mCenterPoint.x + mr / gen2, mCenterPoint.y - mr / gen2);
        mPointD = new PointF(mCenterPoint.x - mr / gen2, mCenterPoint.y - mr / gen2);

        mPointE = new PointF(mCenterPoint.x - mR / gen2, mCenterPoint.y + mR / gen2);
        mPointF = new PointF(mCenterPoint.x + mR / gen2, mCenterPoint.y + mR / gen2);
        mPointG = new PointF(mCenterPoint.x + mr / gen2, mCenterPoint.y + mr / gen2);
        mPointH = new PointF(mCenterPoint.x - mr / gen2, mCenterPoint.y + mr / gen2);
        Log.d("tag", mPointA.x + "mPointA.X" + mPointA.y + "mPointA.y");
        Log.d("tag", mPointB.x + "mPointB.X" + mPointB.y + "mPointB.y");

        /**画圆弧块*/
        //左侧
        drawSrcBlock(mChoseBlock == Block.TOP, mPointA, -135, mPointC, -45, canvas);
        //顶部
        drawSrcBlock(mChoseBlock == Block.RIGHT, mPointB, -45, mPointG, 45, canvas);
        //右侧
        drawSrcBlock(mChoseBlock == BOTTOM, mPointF, 45, mPointH, 135, canvas);
        //下册
        drawSrcBlock(mChoseBlock == LEFT, mPointE, 135, mPointD, 225, canvas);

       /**画中心圆,方便上色*/
        drawCircle(mChoseBlock == Block.CENTER, mCenterPoint, mr, canvas);

        //三角形高
        mTriHeight = 1.0f / 3.0f * (mR - mr);
        //三角形顶端距离圆心距离
        disForCenter = mr + 2f * mTriHeight;

        /**画四个箭头*/

        PointF top1 = new PointF(mCenterPoint.x, mCenterPoint.y - disForCenter);
        PointF top2 = new PointF(mCenterPoint.x + mTriHeight,
                mCenterPoint.y - disForCenter + mTriHeight);
        PointF top3 = new PointF(mCenterPoint.x - mTriHeight,
                mCenterPoint.y - disForCenter + mTriHeight);
        drawTriangle(mChoseBlock == Block.TOP, top1, top2, top3, canvas);

        PointF left1 = new PointF(mCenterPoint.x - disForCenter, mCenterPoint.y);
        PointF left2 = new PointF(mCenterPoint.x - disForCenter + mTriHeight,
                mCenterPoint.y + mTriHeight);
        PointF left3 = new PointF(mCenterPoint.x - disForCenter + mTriHeight,
                mCenterPoint.y - mTriHeight);
        drawTriangle(mChoseBlock == LEFT, left1, left2, left3, canvas);

        PointF bottom1 = new PointF(mCenterPoint.x, mCenterPoint.y + disForCenter);
        PointF bottom2 = new PointF(mCenterPoint.x + mTriHeight, mCenterPoint.y+disForCenter - mTriHeight);
        PointF bottom3 = new PointF(mCenterPoint.x - mTriHeight,
                mCenterPoint.y + disForCenter - mTriHeight);
        drawTriangle(mChoseBlock ==  BOTTOM, bottom1, bottom2, bottom3, canvas);

        PointF right1 = new PointF(mCenterPoint.x + disForCenter,
                mCenterPoint.y);
        PointF right2 = new PointF(mCenterPoint.x + disForCenter - mTriHeight,
                mCenterPoint.y+mTriHeight);
        PointF right3 = new PointF(mCenterPoint.x + disForCenter - mTriHeight,
                mCenterPoint.y -mTriHeight);
        drawTriangle(mChoseBlock == Block.RIGHT, right1, right2, right3, canvas);

/*        canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, 200, mPaint);
        canvas.drawCircle(mCenterPoint.x, mCenterPoint.y, 75, mPaint);
        canvas.drawLine(mPointA.x, mPointA.y, mPointD.x, mPointD.y, mPaint);
        canvas.drawLine(mPointB.x, mPointB.y, mPointC.x, mPointC.y, mPaint);
        canvas.drawLine(mPointE.x, mPointE.y, mPointH.x, mPointH.y, mPaint);
        canvas.drawLine(mPointF.x, mPointF.y, mPointG.x, mPointG.y, mPaint);*/
    }
/**画箭头*/
    private void drawTriangle(boolean isSlide, PointF p1, PointF p2, PointF p3,
            Canvas canvas) {
        mPaint.setStyle(Paint.Style.FILL);  //填充中间
        if (isSlide) {
            mPaint.setColor(mArrowChoseColor);
        } else {
            mPaint.setColor(mArrowColor);
        }
        Path path = new Path();
        path.moveTo(p1.x, p1.y);
        path.lineTo(p2.x, p2.y);
        path.lineTo(p3.x, p3.y);
        path.close();
        canvas.drawPath(path, mPaint);
    }
/** 画圆  中心小圆*/
    private void drawCircle(boolean isSlide, PointF centerPoint, float mr, Canvas canvas) {
            mPaint.setColor(mLineColor);
        if (isSlide) {
            mPaint.setStyle(Paint.Style.FILL); //填充
        } else {
            mPaint.setStyle(Paint.Style.STROKE);
        }
        canvas.drawCircle(centerPoint.x, centerPoint.y, mr, mPaint);

        Rect bounds = new Rect();
        mPaint.getTextBounds(mText, 0, mText.length(), bounds);
        int textwidth = bounds.width();
        int textHeight = bounds.height();
        if (isSlide) {
            mPaint.setColor(mArrowChoseColor);
        } else {
            mPaint.setColor(mArrowColor);
        }
        canvas.drawText(mText, mCenterPoint.x - textwidth * 0.5f, mCenterPoint.y + textHeight * 0.5f,
                mPaint);
        mPaint.setColor(mLineColor);
    }
/** 画扇形边框*/
    private void drawSrcBlock(boolean isSolid, PointF pointA, int startAngle, PointF pointC,
            int startAngle1,
            Canvas canvas) {
        if (isSolid) {
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(mChoseColor);
        } else {
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setColor(mLineColor);
        }
        Path path = new Path();
        path.moveTo(pointA.x, pointA.y);
        path.arcTo(new RectF(0, 0, mWidth, mWidth), startAngle, 90);
        path.lineTo(pointC.x, pointC.y);
        path.arcTo(new RectF(mWidth * 0.5f * 125f / 200f, mHeight * 0.5f * 125f / 200f,
                mWidth * 0.5f * 275f / 200f, mWidth * 0.5f * 275f / 200f), startAngle1, -90);
        path.close();
        canvas.drawPath(path, mPaint);
    }

step5.重写onTouchEvent对触摸事件进行处理,更改对应标志位,并将触摸状态回调出去

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

            float x = event.getX();
            float y = event.getY();
                evaluateTouchBlock(x, y);
                break;
            case MotionEvent.ACTION_MOVE:

                break;
            case MotionEvent.ACTION_UP:
                mChoseBlock = NULL;
                invalidate();
                break;
        }


        return true;
    }

    //计算点的触摸位置块
    private void evaluateTouchBlock(float x, float y) {

        float dis = GeometryUtil.getDistanceBetween2Points(mCenterPoint,
                new PointF(x, y));
        if (dis >= mr && dis <= mR) { //大小圆之间

            float datY = y - mCenterPoint.y;
            float datX = x - mCenterPoint.x;

            if (datX==0) {//排除斜率为0状况
                if (datY > 0) {
                    mChoseBlock = BOTTOM;
                } else {
                    mChoseBlock = Block.TOP;
                }
            } else {

                float k = datY / datX;

                if ((k >= 1 || k <= -1)) {
                    if (datY <= 0) {
                        mChoseBlock = Block.TOP; //LEFT
                    } else {
                        mChoseBlock = BOTTOM; //RIGHT
                    }
                } else {
                    if (datX <= 0) {
                        mChoseBlock = LEFT; //BOTTOM
                    } else {
                        mChoseBlock = Block.RIGHT;//TOP
                    }
                }
            }
            invalidate();
        } else if (dis < mr) {//小圆
            mChoseBlock = Block.CENTER;
            invalidate();
        } else {

        }
        if (mOnTouchBlockListener != null) {
            switch (mChoseBlock) {

                case LEFT:
                    mOnTouchBlockListener.onLeft();
                    break;
                case TOP:
                    mOnTouchBlockListener.onTop();
                    break;
                case BOTTOM:
                    mOnTouchBlockListener.onBottom();
                    break;
                case RIGHT:
                    mOnTouchBlockListener.onRight();
                    break;
                case CENTER:
                    mOnTouchBlockListener.onCenter();
                    break;
            }
        }
    }

step6.使用到的回调接口,和枚举状态类

    //点击事件回调
    private OnTouchBlockListener mOnTouchBlockListener;

    public void setOnTouchBlockListener(
            OnTouchBlockListener onTouchBlockListener) {
        mOnTouchBlockListener = onTouchBlockListener;
    }

    public interface OnTouchBlockListener {
        void onTop();

        void onBottom();

        void onLeft();

        void onRight();

        void onCenter();
    }
 enum Block {
        LEFT, TOP, RIGHT, BOTTOM, CENTER, NULL
    }

step7.工具类

package com.sy.copymenu;

import android.graphics.PointF;

/**
 * 几何图形工具
 */
public class GeometryUtil {

    /**
     * As meaning of method name.
     * 获得两点之间的距离
     * @param p0
     * @param p1
     * @return
     */
    public static float getDistanceBetween2Points(PointF p0, PointF p1) {
        float distance = (float) Math.sqrt(Math.pow(p0.y - p1.y, 2) + Math.pow(p0.x - p1.x, 2));
        return distance;
    }

    /**
     * Get middle point between p1 and p2.
     * 获得两点连线的中点
     * @param p1
     * @param p2
     * @return
     */
    public static PointF getMiddlePoint(PointF p1, PointF p2) {
        return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f);
    }

    /**
     * Get point between p1 and p2 by percent.
     * 根据百分比获取两点之间的某个点坐标
     * @param p1
     * @param p2
     * @param percent
     * @return
     */
    public static PointF getPointByPercent(PointF p1, PointF p2, float percent) {
        return new PointF(evaluateValue(percent, p1.x , p2.x), evaluateValue(percent, p1.y , p2.y));
    }

    /**
     * 根据分度值,计算从start到end中,fraction位置的值。fraction范围为0 -> 1
     * @param fraction
     * @param start
     * @param end
     * @return
     */
    public static float evaluateValue(float fraction, Number start, Number end){
        return start.floatValue() + (end.floatValue() - start.floatValue()) * fraction;
    }


    /**
     * Get the point of intersection between circle and line.
     * 获取 通过指定圆心,斜率为lineK的直线与圆的交点。
     * 
     * @param pMiddle The circle center point.
     * @param radius The circle radius.
     * @param lineK The slope of line which cross the pMiddle.
     * @return
     */
    public static PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) {
        PointF[] points = new PointF[2];

        float radian, xOffset = 0, yOffset = 0; 
        if(lineK != null){
            radian= (float) Math.atan(lineK);
            xOffset = (float) (Math.sin(radian) * radius);
            yOffset = (float) (Math.cos(radian) * radius);
        }else {
            xOffset = radius;
            yOffset = 0;
        }
        points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset);
        points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset);

        return points;
    }
}

step8. xml

  <com.sy.copymenu.MenuView
        android:id="@+id/menu_view"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_centerInParent="true"
        android:background="@android:color/transparent"

        menuview:arrowChoseColor="@android:color/white"
        menuview:arrowColor="@android:color/holo_red_dark"
        menuview:choseColor="@android:color/black"
        menuview:lineColor="@android:color/black"
        menuview:mtext="OK"
        menuview:mtextSize="20sp"
    />

step9.状态回调监听

  mMenuView = (MenuView) findViewById(R.id.menu_view);
        mMenuView.setOnTouchBlockListener(new MenuView.OnTouchBlockListener() {
            @Override
            public void onTop() {
                ToastUtils.showToast(mContext, "top");
            }

            @Override
            public void onBottom() {
                ToastUtils.showToast(mContext, "onBottom");
            }

            @Override
            public void onLeft() {
                ToastUtils.showToast(mContext, "onLeft");
            }

            @Override
            public void onRight() {
                ToastUtils.showToast(mContext, "onRight");
            }

            @Override
            public void onCenter() {
                ToastUtils.showToast(mContext, "onCenter");
            }
        });