Android自定义View,卫星菜单

一、先看效果图

第二张图是测试显示结果,第一张图是网上找的,效果大概就和第一张最后的那个效果一样。


效果图一.gif

Android自定义View,卫星菜单_第1张图片
10AA2DDF198D76A2DE249170A07F4EBA.jpg
二、自定义View的套路
1. 自定义属性
2. 测量控件的宽高
3. 摆放控件的位置
4. 绘制控件
5. 用户交互(事件处理)
三、分析卫星菜单特有的属性
1. 控件是弧形摆放,所以要有一个弧形的半径。
2. 控件弧形摆放,从第一个摆放到最后一个要有一个角度。
3. 控件展开的背景色。
四、定义卫星菜单特有的属性

    
    
    
    
    
    

五、创建自定义卫星菜单ArcLayout,并获取特有属性
public class ArcLayout extends ViewGroup implements View.OnClickListener {
    // 摆放的总角度
    private float mArcAngle = 100f;
    // 摆放的半径
    private float mArcRadius = 120f;
    // 展开的背景色
    private int mArcSpreadColor = Color.TRANSPARENT;
    // 开始的角度
    private float mStartAngle;
 
    public ArcLayout(Context context) {
        this(context, null);
    }

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

    public ArcLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ArcLayout);
        mArcAngle = array.getFloat(R.styleable.ArcLayout_arcAngle, mArcAngle);
        mArcRadius = array.getDimension(R.styleable.ArcLayout_arcRadius, dip2px(mArcRadius));
        mArcSpreadColor = array.getColor(R.styleable.ArcLayout_arcSpreadColor, mArcSpreadColor);
        // 计算开始的角度
        // rad 是弧度, deg 是角度
        // rad(弧度) = deg(角度) *  PI / 180
        mStartAngle = (float) ((180 - mArcAngle) / 2 * (Math.PI / 180));
        array.recycle();
        // 设置背景色
        setBackgroundColor(mArcSpreadColor);
        mBackground = getBackground();
        if (mBackground != null) {
            mBackground.setAlpha(0);
        }
  
    }
    private float dip2px(float value) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, getResources().getDisplayMetrics());
    }
    
}
六、测量子卫星菜单View的大小 和计算每个子菜单偏移平分的角度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        // 测量子控件的宽高
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 计算平分的角度
    mSquareAngle = (float) (mArcAngle / (childCount - 2) * (Math.PI / 180));
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    // 获取控件的宽高
    mWidth = w;
    mHeight = h;
}
七、卫星菜单控件的摆放

先看摆放示意图


Android自定义View,卫星菜单_第2张图片
先看摆放示意图.png

其中p1x 、p1y是子控件的左上角的坐标。
子控件的摆放分两步走:

  1. 摆放底部中间的子控件
  2. 摆放弧形位置的子菜单控件
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (changed) {
        layoutCenter();
        layoutMenu();
    }
}

/**
 * 摆放第一个控件
 */
private void layoutCenter() {
    mCenterChild = getChildAt(0);
    // 第一个子控件的宽高
    int width = mCenterChild.getMeasuredWidth();
    int height = mCenterChild.getMeasuredHeight();
    int l = mWidth / 2 - width / 2;
    int t = mHeight - height-getPaddingBottom();
    mCenterChild.layout(l, t, l + width, t + height);
    // 第一个子控件的点击事件
    mCenterChild.setOnClickListener(this);
}

/**
 * 摆放子菜单
 */
private void layoutMenu() {
    int childCount = getChildCount();
    if (childCount < 2) {
        return;
    }
    for (int i = 1; i < childCount; i++) {
        View child = getChildAt(i);
        // 子控件的宽高
        int width = child.getMeasuredWidth();
        int height = child.getMeasuredHeight();
        float Q = mStartAngle + mSquareAngle * (i - 1);
        // left = 宽度的一半 - x - 子控件宽度的一半
        // top  = 高度 - y - 子控件高度的一半
        int cl = (int) (mWidth / 2 - Math.cos(Q) * mArcRadius - width / 2);
        int ct = (int) (mHeight - Math.sin(Q) * mArcRadius - height / 2-getPaddingBottom());
        // 摆放子菜单
        child.layout(cl, ct, cl + width, ct + height);

        // 默认隐藏子菜单
        child.setVisibility(GONE);
        final int position = i;
        // 子菜单的点击事件
        child.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                menuItemClick(v, position);
            }
        });

    }
}

其实开始的时候,控件都全部摆放好,默认将子控件都隐藏。

八、处理底部中间的子控件的点击事件
1. 点击后,记录点击状态,如果菜单隐藏,就展开,如果是展开就隐藏。
2. 创建隐藏和打开子菜单的动画
3. 点击后改变菜单的状态 
4. 改变背景色
5. 如果动画执行就屏蔽点击事件等一些细节处理

// 默认关闭
private Status mStatus = Status.CLOSE;
// 菜单的状态
public enum Status {
    CLOSE, OPEN
}
/**
 * 关闭或者打开菜单
 */
public void toggle() {
    // 中间主要的View设置旋转动画
    Animation animation = ArcAnimUtil.rotateCenterAnim(mDuration);
    mCenterChild.startAnimation(animation);
    // 背景动画
    bgAnim();
    // 中间子View设置平移和旋转动画
    int childCount = getChildCount();
    for (int i = 1; i < childCount; i++) {
        final View child = getChildAt(i);
        child.setVisibility(VISIBLE);
        // 子控件的高
        int height = child.getMeasuredHeight();
        float Q = mStartAngle + mSquareAngle * (i - 1);
        // 离当前View X坐标上的距离
        int fromXDelta = (int) (Math.cos(Q) * mArcRadius);
        // 离当前View Y坐标上的距离
        int fromYDelta = (int) (Math.sin(Q) * mArcRadius - height / 2-getPaddingBottom());
        // 动画
        Animation anim = null;
        if (mStatus == Status.CLOSE) {
            // 展开的动画
            anim = ArcAnimUtil
                    .menuAnim(fromXDelta, 0, fromYDelta, 0, i, mDuration);
            child.setEnabled(true);
        } else {
            // 关闭的动画
            anim = ArcAnimUtil
                    .menuAnim(0, fromXDelta, 0, fromYDelta, i, mDuration);
            child.setEnabled(false);
        }
        anim.setAnimationListener(new AnimationAdapter() {
            @Override
            public void onAnimationStart(Animation animation) {
                mIsStart = true;
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                mIsStart = false;
                if (mStatus == Status.CLOSE) {
                    child.clearAnimation();
                    child.setVisibility(GONE);
                    // 关闭的背景设置为透明
                    setClickable(false);
                } else {
                    // 设置背景
                    setClickable(true);
                }
            }
        });
        child.startAnimation(anim);
    }
    // 改变菜单的状态
    changeStatus();
}

/**
 * 背景动画
 */
private void bgAnim() {
    // 背景动画
    ValueAnimator valueAnimator = null;
    if (mStatus == Status.CLOSE) {
        // 展开
        valueAnimator = ObjectAnimator.ofFloat(0, 1);
    } else {
        // 关闭
        valueAnimator = ObjectAnimator.ofFloat(1, 0);
    }
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float value = (float) animation.getAnimatedValue();
            int alpha = (int) (value * 255);
            if (mBackground != null) {
                mBackground.setAlpha(alpha);
            }
        }
    });
    valueAnimator.setDuration(mDuration);
    valueAnimator.start();
}
/**
 * 改变菜单的状态
 */
private void changeStatus() {
    mStatus = mStatus == Status.OPEN ? Status.CLOSE : Status.OPEN;
}
九、处理子菜单的点击事件
1. 执行用户回调监听
2. 执行子菜单的点击动画
3. 执行背景动画
4. 改变菜单的状态 
/**
 * 子菜单的点击事件
 */
private void menuItemClick(View view, int position) {
    if (mIsStart) {
        return;
    }
    if (mOnMenuItemClickListener != null) {
        mOnMenuItemClickListener.onMenuItemClick(view, position);
    }
    // 设置控件不能点击
    setClickable(false);
    int childCount = getChildCount();
    for (int i = 1; i < childCount; i++) {
        View child = getChildAt(i);
        Animation animation = null;
        if (i == position) {
            animation = ArcAnimUtil
                    .menuItemAnim(1f, 1.2f, 1f, 1.2f, mDuration);
        } else {
            animation = ArcAnimUtil
                    .menuItemAnim(1f, 0f, 1f, 0f, mDuration);
        }
        animation.setAnimationListener(new AnimationAdapter() {
            @Override
            public void onAnimationStart(Animation animation) {
                mIsStart = true;
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                mIsStart = false;
            }
        });
        child.startAnimation(animation);
        child.setEnabled(false);
    }

    // 背景动画
    bgAnim();
    // 改变当前状态
    changeStatus();
}
十、动画的帮助类
/**
 * 卫星布局--动画的工具类
 */

public class ArcAnimUtil {
    /**
     * 旋转360度
     */
    public static Animation rotateCenterAnim(int duration) {
        RotateAnimation ta = new RotateAnimation(0, 360,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        ta.setDuration(duration);
        ta.setFillAfter(true);
        return ta;
    }

    public static Animation menuAnim(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta, int position, int duration) {
        AnimationSet set = new AnimationSet(true);
        // romXDelta:动画开始前,离当前View X坐标上的距离。
        // toXDelta:动画结束后,离当前View X坐标上的距离。
        // fromYDelta:动画开始前,离当前View Y坐标上的距离。
        // toYDelta:动画结束后,离当前View Y坐标上的距离。
        Animation transAnim = new TranslateAnimation(fromXDelta, toXDelta, fromYDelta, toYDelta);
        ;
        transAnim.setStartOffset((position * 50));
        RotateAnimation ta = new RotateAnimation(0, 360,
                Animation.RELATIVE_TO_SELF, 0.5f,
                Animation.RELATIVE_TO_SELF, 0.5f);
        set.setDuration(duration);
        set.setFillAfter(true);
        set.addAnimation(ta);
        set.addAnimation(transAnim);
        return set;
    }

    public static Animation menuItemAnim(float fromX, float toX, float fromY, float toY, int duration) {
        AnimationSet set = new AnimationSet(true);
        ScaleAnimation sa = new ScaleAnimation(fromX, toX, fromY, toY,
                Animation.RELATIVE_TO_SELF, Animation.RELATIVE_TO_SELF);
        AlphaAnimation aa = new AlphaAnimation(1.0f, 0);
        set.setDuration(duration);
        set.setFillAfter(true);
        set.addAnimation(sa);
        set.addAnimation(aa);
        return set;
    }
}

动画的代码没什么复杂的,自已去看看就好。

十一、其它的一些处理
1.  菜单展开后,点击控件关闭菜单。
2.  菜单展开后,屏蔽掉控件的点击事件和子菜单的点击事件
3.  其它一点小细节的处理
4.  最后测试,效果图在上面开始的时候

参考了鸿洋大神的代码,感谢鸿洋大神的
Android 自定义ViewGroup手把手教你实现ArcMenu

你可能感兴趣的:(Android自定义View,卫星菜单)