效果图:
这个菜单是我在materialup中看到的一个效果,觉得很不错,就用代码撸了一下,基本还原了UI的效果,如果大家有更好的意见或者优化随时邮件我(认真脸)
设计地址:http://www.material.uplabs.com/posts/alignment-fab-bar思路:
1.画圆角矩形
2.画圆形
3.画中间的X形状
4.点击圆的事件处理
5.点击动画(菜单关闭和打开,圆中间的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-zsin 90°) //x0 是圆心x
@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;