美文网首页
Android圆形菜单

Android圆形菜单

作者: MinRookie | 来源:发表于2017-08-31 17:56 被阅读0次

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

    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<itemSize ;i++){
                //计算扇形中间点的位子
                if (strlist[i] instanceof String){
                    String str = (String) strlist[i];
                    drawText(textf,textradius,offsetAngle,angle,str,canvas,itemSize);
                }else if (strlist[i] instanceof Integer){
                    drawBitmap((int) centerX,(int)centerY,(int)textradius,offsetAngle,angle,(int)strlist[i],canvas);
                }
                offsetAngle += angle;
                AngleMap.put(i,offsetAngle);            //记录每个Itme的位子
            }
    
        }
    

    看到里面有两个方法,一个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 <radiusSize && 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 <radiusSize && 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<Integer,Float> 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; i<itemSize ;i++){
                //计算扇形中间点的位子
                if (strlist[i] instanceof String){
                    String str = (String) strlist[i];
                    drawText(textf,textradius,offsetAngle,angle,str,canvas,itemSize);
                }else if (strlist[i] instanceof Integer){
                    drawBitmap((int) centerX,(int)centerY,(int)textradius,offsetAngle,angle,(int)strlist[i],canvas);
                }
                offsetAngle += angle;
                AngleMap.put(i,offsetAngle);            //记录每个Itme的位子
            }
    
        }
    
        /**
         * 绘制图片
         */
        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);
    
        }
    
        /**
         * 绘制文本
         */
        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);
        }
    
        @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 <radiusSize && 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);
                        }
                        //遍历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圆形菜单

          本文链接:https://www.haomeiwen.com/subject/ozwqjxtx.html