自定义View练习 - 圆形菜单

自定义View练习 - 圆形菜单

来源

自定义view实战笔记–CircleMenu

关键代码都在这篇博客中有说明.

效果图

圆形菜单

主要实现类

CircleMenuLayout.java

自定义ViewGroup的重点是测量和布局

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int measureWidth;
        int measureHeight;
        //获得测量大小和模式
        int size = MeasureSpec.getSize(widthMeasureSpec);
        int mode = MeasureSpec.getMode(widthMeasureSpec);

        if (mode == MeasureSpec.EXACTLY) { //精确模式
            measureWidth = measureHeight = Math.min(size, getDefaultWidth());
        } else { //wrap_content模式
            //获得指定的背景大小,背景有多大,控件就有多大
            int suggestedMinimumWidth = getSuggestedMinimumWidth();
            //如果suggestedMinimumWidth为0, 则没有背景
            if (suggestedMinimumWidth == 0) { //如果没有背景,则设置为默认宽度
                measureWidth = measureHeight = getDefaultWidth();
            } else { //如果有背景,则设置为背景宽和默认宽度的最小值
                measureWidth = measureHeight = Math.min(suggestedMinimumWidth, getDefaultWidth());
            }
        }
        setMeasuredDimension(measureWidth, measureHeight);

        //直径为测量的宽度
        diameter = measureWidth;

        //测量子View
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            //默认系统是通过onMeasure给予MeasureSpec参数的,而对于inflate进来的子视图是没有MeasureSpec参数的
            //因此,我们需要自己设计MeasureSpec,传递给子视图
            int makeMeasureSpec = MeasureSpec.makeMeasureSpec(diameter / 3, MeasureSpec.EXACTLY);
            childView.measure(makeMeasureSpec, makeMeasureSpec);
        }
    }

    /**
     * 获得屏幕宽高中的较小值
     */
    public int getDefaultWidth() {
        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
        int widthPixels = displayMetrics.widthPixels;
        int heightPixels = displayMetrics.heightPixels;
        return Math.min(widthPixels, heightPixels);
    }
  @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            int childWidth = childView.getMeasuredWidth();
            int left = (int) (radius + distance * Math.cos(Math.toRadians(startAngle)) - childWidth / 2);
            int top = (int) (radius + distance * Math.sin(Math.toRadians(startAngle)) - childWidth / 2);
            int right = left + childWidth;
            int bottom = top + childWidth;
            childView.layout(left, top, right, bottom);
            startAngle += 360 / getChildCount();
        }
    }

触摸控件,让子View伴随着滚动. 这里需要重写onTouchEvent.

private float lastX;
    private float lastY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:

                float start = CircleUtil.getAngle(lastX, lastY, diameter); //开始角度
                float end = CircleUtil.getAngle(x, y, diameter); //结束角度
                float angle; //旋转的角度

                //todo 这里有很大的疑问? 为什么这个角度是负数?
                //判断点击的点所处的象限,如果是1,4象限,角度值是正数,否则是负数
                if (CircleUtil.getQuadrant(x, y, diameter) == 1 || CircleUtil.getQuadrant(x, y, diameter) == 4) {
                    angle = end - start;
                } else {
                    angle = start - end;
                }
                startAngle += angle;
                requestLayout();//重新布局
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

其中使用了工具类CircleUtils

public class CircleUtil {
    /**
     * 根据触摸的位置,计算角度
     *
     * @param xTouch
     * @param yTouch
     * @param d 直径
     * @return
     */
    public static float getAngle(float xTouch, float yTouch,int d) {
        double x = xTouch - (d / 2f);
        double y = yTouch - (d / 2f);
        //hypot:通过直角边,求斜边
        return (float) (Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI);
    }

    /**
     * 根据当前位置计算象限
     *
     * @param x
     * @param y
     * @param d 直径
     * @return
     */
    public static int getQuadrant(float x, float y,int d) {
        int tmpX = (int) (x - d / 2);
        int tmpY = (int) (y - d / 2);
        if (tmpX >= 0) {
            return tmpY >= 0 ? 4 : 1;
        } else {
            return tmpY >= 0 ? 3 : 2;
        }

    }
}

学到的知识

  1. 在自定义ViewGroup中不仅要测量自己的宽高, 还需要测量子View的大小.
  2. 重写onLayout主要是对子View坐标的计算. 通过规律获得坐标值.
  3. onTouchEvent返回true, 表示当前控件消费了touch事件.

遗留的问题

  1. 在onTouchEvent中为什么在第一和第四象限计算角度会是正数,而其他象限是负数? 我通过打印日志看到, end角度在 -90度 ~0和0~ 90度 之间变化. 这个规律不是很清楚.

github地址: https://github.com/cizkey/CustomPractice/tree/master/CircleMenu

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值