Android圆形菜单

首先在这里感谢三位大佬 Jason,Hello ,403,还有同事以及初中学霸的帮助(对角度的算法,提供)。
两位大佬的传送门:
Jason:http://my.csdn.net/luofen521
Hello:http://www.jianshu.com/u/cff42ea9b87a
先说说我们的效果

Android圆形菜单_第1张图片
DEF2677E-8A6D-4A80-8F40-4353DA49CFD4.png

前言:大体效果就是这样的,对,也许你们说,不是就一个圆形菜单么。没错,就是一个圆形菜单,但是这个圆形菜单是你点击那里,就显示到哪里。当时老板出这个,有一种想死的心,在网上找遍了,没有gitHub上面也没有,而且我还没怎么写过自定义,没办法,老板提出来,出不来,就要GG,硬着头皮上(没写过自定义View的同学不要害怕,可以自己试试)

首先:新建一个View,我取名为CircleMenu
然后实现三个构造法方法

public CircleMenu(Context context) {
     this(context,null);
 }

 public CircleMenu(Context context, @Nullable AttributeSet attrs) {
     this(context, attrs,0);
 }

 public CircleMenu(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
     super(context, attrs, defStyleAttr);
     mResources = getResources();
 }

都让它调用三个参数的构造方法,我们看到,在第三个构造方法里面初始化了Resources,因为得到图片要。
然后我们要想,如果要实现一个圆形菜单,我们首先的要实现什么,然后要实现什么。
1.画一个圆出来
2.然后画一个圆环出来
3.把文字和图片添加到圆环里面去
4.修改菜单点击的位子
5.实现菜单的点击事件
第一步:画圆(我们把圆环一起画出来)
在onMeasure方法里面初始化画笔以及圆的半径,还有画笔的宽度

 //控制圆显示最大的大小,最大为200,最小为宽高最小值的三分之一
        if (Math.min(getMeasuredWidth(), getMeasuredHeight())  > 200){
            abroadRadius = 200;
        }else {
            abroadRadius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 3;
        }
 paintSize = abroadRadius / 3;           //取画笔的宽度为半径的三分之一
 abrodPaint = new Paint();
        abrodPaint.setColor(abroadBgColor);
        abrodPaint.setAlpha(50);
        abrodPaint.setStrokeWidth(paintSize);
        abrodPaint.setAntiAlias(false);
        abrodPaint.setStyle(Paint.Style.STROKE);

然后再onDraw方法里面把圆画出来,这里的X,Y都是取的控件的中心位置
X = getMeasuredWidth() / 2f;
Y = getMeasuredHeight() / 2f;

 canvas.drawCircle(X,Y,abroadRadius,abrodPaint);

这样,我们的圆和圆环就一起画出来的,这里要注意一个事情,就是,圆环的大小,因为圆环是利用画笔加宽画出来的,我们看到的圆环外边的半径是要根据半径加画笔一半,就是我们的圆环外边到圆中心的半径。
第三部,添加文字和图片
图片和文字,怎么放在一起呢,我用一个数组Object[]放进去的,然后再判断类型,文字为String类型,图片为int类型
写一个方法canvasText(Canvas canvas),在onDraw里面去调用

private void canvasText(Canvas canvas) {
        //计算每个占位的角度
        int itemSize =  strlist.length;
        angle = 360f / itemSize;
        float centerX = X;//中点
        float centerY = Y;
        Log.e("CircleMenu:","X:"+X + "   Y:"+Y);
         textradius = abroadRadius;
        //计算添加文字的区域
        final RectF textf = new RectF(centerX - textradius,centerY - textradius, centerX + textradius, centerY + textradius);
        for (int i = 0; i

看到里面有两个方法,一个drawText和drawBitmap,一个是添加文字,一个是添加图片,添加文字,我是用的鸿神的算法

 /**
     * 绘制文本
     */
    private void drawText(RectF mRange,float mRadius,float startAngle, float sweepAngle,
                          String string,Canvas canvas,int mItemCount)
    {
        Path path = new Path();
        path.addArc(mRange, startAngle, sweepAngle);
        float textWidth = textPaint.measureText(string);
        // 利用水平偏移让文字居中
        float hOffset = (float) (mRadius * Math.PI / mItemCount / 2 - textWidth / 2);// 水平偏移
        float vOffset = mRadius / 2 / 6;// 垂直偏移
        canvas.drawTextOnPath(string, path, hOffset, vOffset, textPaint);
    }
   /**
     * 绘制图片
     */
    private void drawBitmap(int X,int Y ,float mRadius,float offsetAngle,float angle, int img, Canvas canvas) {
        int imgWidth = (int) mRadius / 6;
        float x = (float) (X + mRadius * Math.cos(Math.toRadians(offsetAngle+(angle / 4))));
        float y = (float) (Y + mRadius * Math.sin(Math.toRadians(offsetAngle+(angle / 4))));
        RectF  rectf = new RectF(x - imgWidth *2/ 3, y - imgWidth*2 / 3, x + imgWidth
                *2/ 3, y + imgWidth*2/3);
        Bitmap bitmap =((BitmapDrawable) mResources.getDrawable(img)).getBitmap();
        canvas.drawBitmap(bitmap, null, rectf, null);

    }donw

好了,就这样,我们的圆形菜单初步完成了
点击的位子就好办了,我们首先,onTouchEvent方法得到MotionEvent.ACTION_DOWN获得点击的downX,downY,然后把我们的中心,把我们的X,Y设置为donwX,donwY,然后调用 invalidate();从新绘制就好了,这样,我们的菜单就初步完成了。然后就是我们的点击方法,提供给外部,所以我们写一个接口,然后实现点击方法
接口:

public interface CircleOnClickItemListener {
    void onItem(View view,int pos);
}

然后我们再实Item的点击方法,我们看到在canvasText方法里面有一句代码 AngleMap.put(i,offsetAngle); 这个就是我们记录每个Item的角度,位子,以及我们点击的哪个

 @Override
    public boolean onTouchEvent(MotionEvent event) {

        float downX;
        float downY;
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                //防止按下直接消失,所以我们这里直接返回为true
                return true;
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();
                Log.e("event", "ACTION_DOWN:X:" + downX + "    Y:" + downY);
                float distanceX = Math.abs(X-downX);
                float distanceY = Math.abs(Y- downY);
                float distanceZ = (float) Math.sqrt(Math.pow(distanceX,2) + Math.pow(distanceY,2));
                if (distanceZ size){
                    float radius = 0;
                    // 第一象限
                    if (downX >= getMeasuredWidth() / 2 && downY >= getMeasuredHeight() / 2) {
                        Log.e("event", "ACTION_DOWN:X:" + downX + "    Y:" + downY);
                        radius = (int) (Math.atan((downY - getMeasuredHeight() / 2) * 1.0f
                                / (downX - getMeasuredWidth() / 2)) * 180 / Math.PI);
                    }
                    // 第二象限
                    if (downX <= getMeasuredWidth() / 2 && downY >= getMeasuredHeight() / 2) {
                        Log.e("event", "ACTION_DOWN:X:" + downX + "    Y:" + downY);
                        radius = (int) (Math.atan((getMeasuredWidth() / 2 - downX)
                                / (downY - getMeasuredHeight() / 2))
                                * 180 / Math.PI + 90);
                    }
                    // 第三象限
                    if (downX <= getMeasuredWidth() / 2 && downY <= getMeasuredHeight() / 2) {
                        Log.e("event", "ACTION_DOWN:X:" + downX + "    Y:" + downY);
                        radius = (int) (Math.atan((getMeasuredHeight() / 2 - downY)
                                / (getMeasuredWidth() / 2 - downX))
                                * 180 / Math.PI + 180);
                    }
                    // 第四象限
                    if (downX >= getMeasuredWidth() / 2 && downY <= getMeasuredHeight() / 2) {
                        Log.e("event", "ACTION_DOWN:X:" + downX + "    Y:" + downY);
                        radius = (int) (Math.atan((downX - getMeasuredWidth() / 2)
                                / (getMeasuredHeight() / 2 - downY))
                                * 180 / Math.PI + 270);
                    }
                    for (int i : AngleMap.keySet()){
                       int x =  (int)(AngleMap.get(i) - angle);
                        if (radius > AngleMap.get(i) - angle && radius < AngleMap.get(i)){
                            Log.e("event","x:"+x + "     y:"+AngleMap.get(i) + "    radius:"+radius);
                            circleOnClickItemListener.onItem(this,i);
                            break;
                        }
                    }
                    return true;
                }

                break;
        }


        return super.onTouchEvent(event);
    }

里面有段代码,这个作用,就是要去判断是否点击的时候圆弧内

   float distanceX = Math.abs(X-downX);
                float distanceY = Math.abs(Y- downY);
                float distanceZ = (float) Math.sqrt(Math.pow(distanceX,2) + Math.pow(distanceY,2));

在if (distanceZ size)这个判断就是,第一个是我们圆弧的外边位置,第二个size是圆弧内边的位置 size的计算方法,跟外边的计算方法一样,一个是加上画笔宽度的一半,一个是减去画笔的一半。为什么要再 MotionEvent.ACTION_UP:直接返回为true呢,因为需求是,点击的时候显示,不点击不显示。这样,我们的基本需求已经完善了,接下来,就是,点击的时候显示,在点击就隐藏。先说说,我们的圆形菜单,因为是在页面的上,所以我就是在Activity里面的onTouchEvent方法写的操作,接下来我就把所有源码附上

public class CircleMenu extends View {

    private int abroadRadius  ;           //外部半径
    private Paint abrodPaint ;          //外部圆画笔
    private float paintSize ;           //画笔宽度
    private float offsetAngle = 0;          //初始角度

    private Paint textPaint;            //文字画笔

    private Paint bitmapPaint;              //图片画笔
    private Resources mResources;

    private float textradius;
    private float radiusSize;               //半径+画笔的宽度一半 = 看到圆的半径
    private float size ;          //内边到外边距的大小

    private Map AngleMap;            //记录每个Itme的角度值

    /**
     * 文字的大小
     */
    private float mTextSize = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP, 15, getResources().getDisplayMetrics());

    private static int abroadBgColor = 0xff489cf0;          //圆形菜单颜色

    /**
     * 菜单Item
     */
    private Object[] strlist = new Object[]{"item2","item3",R.drawable.set,R.drawable.set,R.drawable.set,"item4","item5","item6"};

    private float X = 100;          //默认位置
    private float Y = 100;
    private float angle;            //每个Item所占的角度
    public CircleMenu(Context context) {
        this(context,null);
    }

    public CircleMenu(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CircleMenu(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mResources = getResources();
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //控制圆显示最大的大小,最大为200,最小为宽高最小值的三分之一
        if (Math.min(getMeasuredWidth(), getMeasuredHeight())  > 200){
            abroadRadius = 200;
        }else {
            abroadRadius = Math.min(getMeasuredWidth(), getMeasuredHeight()) / 3;
        }
        X  = getMeasuredWidth() / 2f;
        Y =  getMeasuredHeight() / 2f;
        radiusSize  = abroadRadius + (paintSize/2);
        size = radiusSize - paintSize;
        Log.e("event","中心点X:"+X+ "    中心点Y:"+Y + "     半径:"+abroadRadius);

        AngleMap = new HashMap<>();
        paintSize = abroadRadius / 3;           //取画笔的宽度为半径的三分之一

        abrodPaint = new Paint();
        abrodPaint.setColor(abroadBgColor);
        abrodPaint.setAlpha(50);
        abrodPaint.setStrokeWidth(paintSize);
        abrodPaint.setAntiAlias(false);
        abrodPaint.setStyle(Paint.Style.STROKE);



        textPaint = new Paint();
        textPaint.setColor(Color.BLACK);
        textPaint.setAntiAlias(false);
        textPaint.setTextSize(mTextSize);

        bitmapPaint = new Paint();
        bitmapPaint.setFilterBitmap(true);
        bitmapPaint.setDither(true);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(X,Y,abroadRadius,abrodPaint);
        canvasText(canvas);
    }

    private void canvasText(Canvas canvas) {
        //计算每个占位的角度
        int itemSize =  strlist.length;
        angle = 360f / itemSize;
        float centerX = X;//中点
        float centerY = Y;
        Log.e("CircleMenu:","X:"+X + "   Y:"+Y);
         textradius = abroadRadius;
        //计算添加文字的区域
        final RectF textf = new RectF(centerX - textradius,centerY - textradius, centerX + textradius, centerY + textradius);
        for (int i = 0; isize){
                    float radius = 0;
                    // 第一象限
                    if (downX >= getMeasuredWidth() / 2 && downY >= getMeasuredHeight() / 2) {
                        Log.e("event", "ACTION_DOWN:X:" + downX + "    Y:" + downY);
                        radius = (int) (Math.atan((downY - getMeasuredHeight() / 2) * 1.0f
                                / (downX - getMeasuredWidth() / 2)) * 180 / Math.PI);
                    }
                    // 第二象限
                    if (downX <= getMeasuredWidth() / 2 && downY >= getMeasuredHeight() / 2) {
                        Log.e("event", "ACTION_DOWN:X:" + downX + "    Y:" + downY);
                        radius = (int) (Math.atan((getMeasuredWidth() / 2 - downX)
                                / (downY - getMeasuredHeight() / 2))
                                * 180 / Math.PI + 90);
                    }
                    // 第三象限
                    if (downX <= getMeasuredWidth() / 2 && downY <= getMeasuredHeight() / 2) {
                        Log.e("event", "ACTION_DOWN:X:" + downX + "    Y:" + downY);
                        radius = (int) (Math.atan((getMeasuredHeight() / 2 - downY)
                                / (getMeasuredWidth() / 2 - downX))
                                * 180 / Math.PI + 180);
                    }
                    // 第四象限
                    if (downX >= getMeasuredWidth() / 2 && downY <= getMeasuredHeight() / 2) {
                        Log.e("event", "ACTION_DOWN:X:" + downX + "    Y:" + downY);
                        radius = (int) (Math.atan((downX - getMeasuredWidth() / 2)
                                / (getMeasuredHeight() / 2 - downY))
                                * 180 / Math.PI + 270);
                    }
                    //遍历Map
                    for (int i : AngleMap.keySet()){
                       int x =  (int)(AngleMap.get(i) - angle);
                        //判断点击的位置,是否在item的区间
                        if (radius > AngleMap.get(i) - angle && radius < AngleMap.get(i)){
                            Log.e("event","x:"+x + "     y:"+AngleMap.get(i) + "    radius:"+radius);
                            circleOnClickItemListener.onItem(this,i);
                            break;
                        }
                    }
                    return true;
                }

                break;
        }


        return super.onTouchEvent(event);
    }

    //判断显示隐藏
    private boolean isShowView = false;
    public boolean isShowView(){
        return isShowView;
    }
    //外部提供显示隐藏的方法
    public void isShow(float x, float y, float width, float height){
        isShowView = true;
        this.setVisibility(VISIBLE);
        calculation(x,y,width,height);
        AngleMap.clear();
        invalidate();
        AngleMap = new HashMap<>();
        offsetAngle = 0;
    }



    private CircleOnClickItemListener circleOnClickItemListener;
    public void setCircleOnClickItemListener(CircleOnClickItemListener circleOnClickItemListener){
        this.circleOnClickItemListener = circleOnClickItemListener;
    }
    
    //计算,显示的位子
    public void calculation(float downX ,float downY,float width,float height){
        downY =  downY - dip2px(25);            //25位状态的高度。这个我是直接写死的
        
        //防止点击角落的位子,或者边缘的位子,圆弧菜单有有一部分显示不出来,所以我们计算点击的位子,到边缘的距离
        if (downX > width  - radiusSize) {
            X = downX > downX - radiusSize ? width - radiusSize : downX;
        }else {
            X = downX < radiusSize ? radiusSize : downX;
        }
        if (downY >= height  - radiusSize) {
            Y =  height - radiusSize  - dip2px(25);

        }else {
            Y = downY <  radiusSize ? radiusSize : downY ;
        }
    }
    
    //如果显示,就隐藏
    public void chie(){
        isShowView = false;
        this.setVisibility(INVISIBLE);
    }

    private int dip2px(float dpValue) {
        final float scale = getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }



}

Activity的代码

public class MainActivity extends AppCompatActivity {


    private CircleMenu circleMenu;
    private int screenWidth;
    private int screenHeight;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DisplayMetrics dm = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(dm);
        //宽度
         screenWidth = dm.widthPixels;
        //高度
         screenHeight = dm.heightPixels;
        circleMenu = (CircleMenu) findViewById(R.id.menu_circle);
        circleMenu.setCircleOnClickItemListener(new CircleOnClickItemListener() {
            @Override
            public void onItem(View view, int pos) {
                Toast.makeText(MainActivity.this,pos+"",Toast.LENGTH_SHORT).show();
            }
        });
    }

    float downX;
    float downY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                if (circleMenu != null){
                    if (!circleMenu.isShowView()){
                        //传入,点击的位子,和页面的宽高
                        circleMenu.isShow(downX,downY,screenWidth,screenHeight);
                    }else {
                        circleMenu.chie();
                    }
                }
                break;
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();
                downY = event.getY();
                break;
        }
        return super.onTouchEvent(event);

    }


}

总结:第一次自己写自定义View ,百度了很多,也问了几位大佬。其他在复杂的控件,不要害怕,网上没有,我们就自己写,自己写的话,首先要理解这个控件都有什么功能,然后根据功能去分步实现,第一步到最后一步,从什么开始实现,怎么去做,不要怕,程序猿千万不要被bug吓到。撸起袖子就是干...程序猿没有怂的....

你可能感兴趣的:(Android圆形菜单)