纯手工打造一个炫酷的菜单 View

Android · songmeitao · 于 发布 · 最后由 heimagogo回复 · 2130 次阅读
1723

效果图:

效果图

这个菜单是我在materialup中看到的一个效果,觉得很不错,就用代码撸了一下,基本还原了UI的效果
设计地址:http://www.material.uplabs.com/posts/alignment-fab-bar

  • 思路
    • 画圆角矩形
    • 画圆形
    • 画中间的X形状
    • 点击圆的事件处理
    • 点击动画(菜单关闭和打开,圆中间的X形状和V形状之间的转换)
 private Paint mBluePaint;

    private Paint mPurplePaint;

    private Paint mWPaint;

    private int mWidth;//宽

    private int mHeight;//高

    private int mRadius;//半径

    private int mXCenter;//x圆心

    private int mYCenter;//y圆心

    private int mMenuRectTop = 20;//菜单矩形的高度

    private int mMenuRectLeftFinal = 80;//50

    private int mMenuRectLeft ;

    private int mMenuRound = 40;//菜单矩形圆角  40

    private int mLen = 15;

    private int mWradio = 8;

    private final int STATE_OPEN = 0;

    private final int STATE_CLOSE = 1;

    private int STATE = STATE_OPEN;
    private int x1;
    private int y1;
    private int x3;
    private int y3;
    private int x2;
    private int y2;
    private int x4;
    private int y4;
    private int finalX1;
    private int finalX4;

    private int mMenuColor=Color.parseColor("#AB47BC");//默认紫色
    private int mCircleColor = Color.parseColor("#288AFF");//默认蓝色
    private int mIconColor = Color.parseColor("#ffffff");//默认白色

    private OnMenuOnClickListener mOnClickListener;


    public FabBarView(Context context) {
        this(context, null);

    }

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


    public FabBarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mBluePaint = new Paint();
        mBluePaint.setAntiAlias(true);
        mBluePaint.setStyle(Paint.Style.FILL);
        mBluePaint.setStrokeWidth(10);
        mBluePaint.setColor(mCircleColor);

        mPurplePaint = new Paint();
        mPurplePaint.setAntiAlias(true);
        mPurplePaint.setStyle(Paint.Style.FILL);
        mPurplePaint.setStrokeWidth(10);
        mPurplePaint.setColor(mMenuColor);

        mWPaint = new Paint();
        mWPaint.setAntiAlias(true);
        mWPaint.setStyle(Paint.Style.FILL);
        mWPaint.setStrokeWidth(17);
        mWPaint.setColor(mIconColor);


    }

上面的代码主要是一些常量和Paint的初始化

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

        mWidth = MeasureSpec.getSize(widthMeasureSpec);
        mHeight = MeasureSpec.getSize(heightMeasureSpec) - 5;
        mXCenter = mWidth / 2;
        mYCenter = mHeight / 2;//圆心按照高来计算
        mRadius = mHeight / 2;

        mMenuRectLeft=mMenuRectLeftFinal;//菜单矩形的框度,可变

        finalX1 = (int) (mXCenter + mLen * Math.sqrt(2));//固定的x1
        x1 = finalX1;//可变的s1
        y1 = (int) (mYCenter - mLen * Math.sqrt(2));
        x3 = (int) (mXCenter - mLen * Math.sqrt(2));
        y3 = (int) (mYCenter + mLen * Math.sqrt(2));
        x2 = (int) (mXCenter + mLen * Math.sqrt(2));
        y2 = (int) (mYCenter + mLen * Math.sqrt(2));
        finalX4 = (int) (mXCenter - mLen * Math.sqrt(2));//固定的x4
        x4 = finalX4;
        y4 = (int) (mYCenter - mLen * Math.sqrt(2));
    }

给我们上面初始化的常量赋值,mMenuRectLeft是矩形的left,也就是距离左变的距离,把mMenuRectLeftFinal赋值给mMenuRectLeft是因为mMenuRectLeftFinal我们会在和面做相应的加减处理,需要记住最初的left值,x1,finalX1 ,x4,finalX4同理;x1,y1-x4,y4 是圆中四个点的坐标,因为我们要在圆内画一个X的形状,可以看下面的草图,我们知道圆心AO、BO、CO、DO这四条线段的长度,也知道每个角的角度(角度为90°),根据这3个条件我们就可以求出 ABCD这四个坐标点,拿(x1,y1)的坐标点的具体算法就是:
(x0 -z *cos 90°,y0-z*sin 90°) //x0 是圆心x

图1

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        RectF menuRect = new RectF(mMenuRectLeft, mMenuRectTop, mWidth - mMenuRectLeft, mHeight - mMenuRectTop);
        canvas.drawRoundRect(menuRect, mMenuRound, mMenuRound, mPurplePaint);

        canvas.drawCircle(mXCenter, mYCenter, mRadius, mBluePaint);

        canvas.drawCircle(x1, y1, mWradio, mWPaint);
        canvas.drawCircle(x3, y3, mWradio, mWPaint);
        canvas.drawCircle(x2, y2, mWradio, mWPaint);
        canvas.drawCircle(x4, y4, mWradio, mWPaint);
        canvas.drawLine(x1, y1, x3, y3, mWPaint);
        canvas.drawLine(x2, y2, x4, y4, mWPaint);

    }

接下来就是onDraw ,这里面比较简单,先创建一个RectF用来存储矩形的左上右下,分别对应下图中的ABCD,然后是画圆,最后就是把在onMeasure中求出圆内4个点的坐标链接起来,然后在每个点上画圆,这样我们的X形状就出来了;

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

                int clickX = (int) event.getRawX();
                int clickY = (int) event.getRawY();

                int[] location = new int[2];
                this.getLocationInWindow(location);
                //XY是控件在屏幕的XY,而不是定义的圆心
                int cenY = location[1] + mYCenter;

                if (Math.pow(mXCenter - clickX, 2) + Math.pow(cenY - clickY, 2) <= Math.pow(mRadius, 2)) {
                    doAnim();
                    if (mOnClickListener != null) {
                        mOnClickListener.onClick(this);
                    }
                    return true;
                }
                break;

            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }

        return true;
    }

    /**
     * 设置点击事件,用于处理打开或关闭菜单的其他事件
     *
     * @param listener
     */
    public void setOnMenuOnClickListener(OnMenuOnClickListener listener) {
        this.mOnClickListener = listener;
    }
    /**
     * 菜单点击事件
     */
    public static interface OnMenuOnClickListener {
        public void onClick(View v);
    }

这个View是个菜单,和用户是有交互的,所以我们就要处理它的点击事件,千万不要直接给这个View设置点击事件,如果你设置了整个View不管你点哪里都会接收这个事件,这样交互就很恶心了,我们要让用户点击中间的圆形接收事件,点击View的其他位置不做处理,算法是:
(圆心x-点击x)平方 + (圆心y - 点击y)平方<圆半径的平方
注:圆心x和圆心y都是圆在整个屏幕上的坐标,可以获取当前控件的位置,然后加上原坐标,获取出圆在屏幕上的坐标,还要留出一个监听,方便做一些其他逻辑的处理


 private void doAnim() {
        switch (STATE) {
            case STATE_OPEN:
                //当前是打开,关闭操作
                //  (View 的宽 - 圆半径)/2 - left
                performAnimate(mMenuRectLeft, (mWidth - mRadius) / 2 - (0) );
                performAnimateX1(x1, mXCenter);
                performAnimateX4(x4, mXCenter);

                STATE = STATE_CLOSE;
                break;
            case STATE_CLOSE:
                //当前是关闭,打开操作
                performAnimate(mMenuRectLeft, mMenuRectLeftFinal);
                performAnimateX1(mXCenter, finalX1);
                performAnimateX4(mXCenter, finalX4);

                STATE = STATE_OPEN;
                break;
        }
    }


 private void performAnimate(final int start, final int end) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            //持有一个IntEvaluator对象,方便下面估值的时候使用
            private IntEvaluator mEvaluator = new IntEvaluator();

            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
                //获得当前动画的进度值,整型,1-100之间
                int currentValue = (Integer) animator.getAnimatedValue();

                //计算当前进度占整个动画过程的比例,浮点型,0-1之间
                float fraction = currentValue / 100f;

                mMenuRectLeft = mEvaluator.evaluate(fraction, start, end);
                invalidate();
            }
        });
        valueAnimator.setInterpolator(new AnticipateOvershootInterpolator());
        valueAnimator.setDuration(1000).start();
    }

    private void performAnimateX1(final int start, final int end) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            private IntEvaluator mEvaluator = new IntEvaluator();

            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
                int currentValue = (Integer) animator.getAnimatedValue();
                float fraction = currentValue / 100f;
                x1 = mEvaluator.evaluate(fraction, start, end);
                invalidate();
            }
        });
        valueAnimator.setInterpolator(new AnticipateOvershootInterpolator());
        valueAnimator.setDuration(1000).start();
    }

    private void performAnimateX4(final int start, final int end) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            private IntEvaluator mEvaluator = new IntEvaluator();

            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
                int currentValue = (Integer) animator.getAnimatedValue();
                float fraction = currentValue / 100f;
                x4 = mEvaluator.evaluate(fraction, start, end);
                invalidate();
            }
        });
        valueAnimator.setInterpolator(new AnticipateOvershootInterpolator());
        valueAnimator.setDuration(1000).start();
    }

最后一步就是动画效果,这里用的是valueAnimator;第一个动画是菜单的闭动画,具体的操作方法就是操作矩形的left值,也就是图2中的A线段,开始值是 left,结束值是图2中的E值,在1000毫秒中增大left,打开动画传入的是 E值和固定的left值,每次递减left的值,因为关闭动画我们是加大left,所以打开也就是减少left的值,通俗一些就是根据left的值让View显示和隐藏;X形状和V形状之前的动画是控件图1中的AD点,A和D点都在一定的时间中移动到圆心的X点,也就是mXCenter;

图2

欢迎大家加入QQ交流群

QQ交流群

我的Github博客

传送门

炫酷的菜单基本就搞定了!源码已经上传到Github,求Start!!!!

本帖已被设为精华帖!
共收到 3 条回复
30
d_clock · #1 ·

不错哦,社区鼓励这种创作性的分享!入选明天Diycode每日精选,继续加油!

96
heimagogo · #3 ·

guli

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册