Android自定義View,衛(wèi)星菜單

一、先看效果圖

第二張圖是測(cè)試顯示結(jié)果,第一張圖是網(wǎng)上找的,效果大概就和第一張最后的那個(gè)效果一樣。


效果圖一.gif

10AA2DDF198D76A2DE249170A07F4EBA.jpg
二、自定義View的套路
1. 自定義屬性
2. 測(cè)量控件的寬高
3. 擺放控件的位置
4. 繪制控件
5. 用戶交互(事件處理)
三、分析衛(wèi)星菜單特有的屬性
1. 控件是弧形擺放,所以要有一個(gè)弧形的半徑。
2. 控件弧形擺放,從第一個(gè)擺放到最后一個(gè)要有一個(gè)角度。
3. 控件展開的背景色。
四、定義衛(wèi)星菜單特有的屬性
<declare-styleable name="ArcLayout">
    <!--擺放的角度:0-180度 -->
    <attr name="arcAngle" format="float" />
    <!--擺放的半徑-->
    <attr name="arcRadius" format="dimension" />
    <!--子菜單展開的背景色-->
    <attr name="arcSpreadColor" format="color" />
</declare-styleable>
五、創(chuàng)建自定義衛(wèi)星菜單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);
        // 計(jì)算開始的角度
        // rad 是弧度, deg 是角度
        // rad(弧度) = deg(角度) *  PI / 180
        mStartAngle = (float) ((180 - mArcAngle) / 2 * (Math.PI / 180));
        array.recycle();
        // 設(shè)置背景色
        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());
    }
    
}
六、測(cè)量子衛(wèi)星菜單View的大小 和計(jì)算每個(gè)子菜單偏移平分的角度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = getChildAt(i);
        // 測(cè)量子控件的寬高
        measureChild(child, widthMeasureSpec, heightMeasureSpec);
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 計(jì)算平分的角度
    mSquareAngle = (float) (mArcAngle / (childCount - 2) * (Math.PI / 180));
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    // 獲取控件的寬高
    mWidth = w;
    mHeight = h;
}
七、衛(wèi)星菜單控件的擺放

先看擺放示意圖


先看擺放示意圖.png

其中p1x 、p1y是子控件的左上角的坐標(biāo)。
子控件的擺放分兩步走:

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

/**
 * 擺放第一個(gè)控件
 */
private void layoutCenter() {
    mCenterChild = getChildAt(0);
    // 第一個(gè)子控件的寬高
    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);
    // 第一個(gè)子控件的點(diǎn)擊事件
    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);

        // 默認(rèn)隱藏子菜單
        child.setVisibility(GONE);
        final int position = i;
        // 子菜單的點(diǎn)擊事件
        child.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                menuItemClick(v, position);
            }
        });

    }
}

其實(shí)開始的時(shí)候,控件都全部擺放好,默認(rèn)將子控件都隱藏。

八、處理底部中間的子控件的點(diǎn)擊事件
1. 點(diǎn)擊后,記錄點(diǎn)擊狀態(tài),如果菜單隱藏,就展開,如果是展開就隱藏。
2. 創(chuàng)建隱藏和打開子菜單的動(dòng)畫
3. 點(diǎn)擊后改變菜單的狀態(tài) 
4. 改變背景色
5. 如果動(dòng)畫執(zhí)行就屏蔽點(diǎn)擊事件等一些細(xì)節(jié)處理

// 默認(rèn)關(guān)閉
private Status mStatus = Status.CLOSE;
// 菜單的狀態(tài)
public enum Status {
    CLOSE, OPEN
}
/**
 * 關(guān)閉或者打開菜單
 */
public void toggle() {
    // 中間主要的View設(shè)置旋轉(zhuǎn)動(dòng)畫
    Animation animation = ArcAnimUtil.rotateCenterAnim(mDuration);
    mCenterChild.startAnimation(animation);
    // 背景動(dòng)畫
    bgAnim();
    // 中間子View設(shè)置平移和旋轉(zhuǎn)動(dòng)畫
    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);
        // 離當(dāng)前View X坐標(biāo)上的距離
        int fromXDelta = (int) (Math.cos(Q) * mArcRadius);
        // 離當(dāng)前View Y坐標(biāo)上的距離
        int fromYDelta = (int) (Math.sin(Q) * mArcRadius - height / 2-getPaddingBottom());
        // 動(dòng)畫
        Animation anim = null;
        if (mStatus == Status.CLOSE) {
            // 展開的動(dòng)畫
            anim = ArcAnimUtil
                    .menuAnim(fromXDelta, 0, fromYDelta, 0, i, mDuration);
            child.setEnabled(true);
        } else {
            // 關(guān)閉的動(dòng)畫
            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);
                    // 關(guān)閉的背景設(shè)置為透明
                    setClickable(false);
                } else {
                    // 設(shè)置背景
                    setClickable(true);
                }
            }
        });
        child.startAnimation(anim);
    }
    // 改變菜單的狀態(tài)
    changeStatus();
}

/**
 * 背景動(dòng)畫
 */
private void bgAnim() {
    // 背景動(dòng)畫
    ValueAnimator valueAnimator = null;
    if (mStatus == Status.CLOSE) {
        // 展開
        valueAnimator = ObjectAnimator.ofFloat(0, 1);
    } else {
        // 關(guān)閉
        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();
}
/**
 * 改變菜單的狀態(tài)
 */
private void changeStatus() {
    mStatus = mStatus == Status.OPEN ? Status.CLOSE : Status.OPEN;
}
九、處理子菜單的點(diǎn)擊事件
1. 執(zhí)行用戶回調(diào)監(jiān)聽
2. 執(zhí)行子菜單的點(diǎn)擊動(dòng)畫
3. 執(zhí)行背景動(dòng)畫
4. 改變菜單的狀態(tài) 
/**
 * 子菜單的點(diǎn)擊事件
 */
private void menuItemClick(View view, int position) {
    if (mIsStart) {
        return;
    }
    if (mOnMenuItemClickListener != null) {
        mOnMenuItemClickListener.onMenuItemClick(view, position);
    }
    // 設(shè)置控件不能點(diǎn)擊
    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);
    }

    // 背景動(dòng)畫
    bgAnim();
    // 改變當(dāng)前狀態(tài)
    changeStatus();
}
十、動(dòng)畫的幫助類
/**
 * 衛(wèi)星布局--動(dòng)畫的工具類
 */

public class ArcAnimUtil {
    /**
     * 旋轉(zhuǎn)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:動(dòng)畫開始前,離當(dāng)前View X坐標(biāo)上的距離。
        // toXDelta:動(dòng)畫結(jié)束后,離當(dāng)前View X坐標(biāo)上的距離。
        // fromYDelta:動(dòng)畫開始前,離當(dāng)前View Y坐標(biāo)上的距離。
        // toYDelta:動(dòng)畫結(jié)束后,離當(dāng)前View Y坐標(biāo)上的距離。
        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;
    }
}

動(dòng)畫的代碼沒什么復(fù)雜的,自已去看看就好。

十一、其它的一些處理
1.  菜單展開后,點(diǎn)擊控件關(guān)閉菜單。
2.  菜單展開后,屏蔽掉控件的點(diǎn)擊事件和子菜單的點(diǎn)擊事件
3.  其它一點(diǎn)小細(xì)節(jié)的處理
4.  最后測(cè)試,效果圖在上面開始的時(shí)候

參考了鴻洋大神的代碼,感謝鴻洋大神的
Android 自定義ViewGroup手把手教你實(shí)現(xiàn)ArcMenu

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容