Android自定义View,卫星菜单

一、先看效果图

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


效果图一.gif

10AA2DDF198D76A2DE249170A07F4EBA.jpg
二、自定义View的套路
1. 自定义属性
2. 测量控件的宽高
3. 摆放控件的位置
4. 绘制控件
5. 用户交互(事件处理)
三、分析卫星菜单特有的属性
1. 控件是弧形摆放,所以要有一个弧形的半径。
2. 控件弧形摆放,从第一个摆放到最后一个要有一个角度。
3. 控件展开的背景色。
四、定义卫星菜单特有的属性
<declare-styleable name="ArcLayout">
    <!--摆放的角度:0-180度 -->
    <attr name="arcAngle" format="float" />
    <!--摆放的半径-->
    <attr name="arcRadius" format="dimension" />
    <!--子菜单展开的背景色-->
    <attr name="arcSpreadColor" format="color" />
</declare-styleable>
五、创建自定义卫星菜单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;
}
七、卫星菜单控件的摆放

先看摆放示意图


先看摆放示意图.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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容