專案需求討論- 自定義圓形排版的ViewGroup來構成動態彈框選單

青蛙要fly發表於2017-08-04

大家好,又到了新的一次需求分析,這次我們的需求是:在不同的條件的前提下,點選一個選單按鈕,出來不同的選單

比如:下面是一系列的公司列表(當然也可以是不同的地區,不同的城市,等等),然後當你選擇好某個之後,我們點選選單按鈕,這時候出來不同的選單

然後我們出來的選單是:

公司1
公司1

公司2
公司2

公司3
公司3

公司四
公司四

5
5

6
6

7
7

然後大家說了。這不是很簡單的事麼。做4個佈局,分別作為這四個公司的選單,然後選擇哪個公司,就彈出哪個公司的選單。
少年,Too Young Too Simple,比如我們一共有10項中的業務,某個A公司有我們的三個功能,然後你前段介面寫死,B公司有五項功能,然後你這時候寫了二個介面,這時候,突然A公司說我升級了。我也要跟B公司一樣有五項功能,然後你又去改介面? 一共有A,B,C...W 公司,難道你就去寫A-W個佈局??(同理,如果是城市劃分,比如在不同的城市可能支援的功能業務不同,出現不同的選單。大城市覆蓋的功能更全,小城市功能更少)

所以這裡我們公司的數量,公司相對於的功能,功能名字,功能圖片名字,都是後臺傳到前端,我們只需要準備一個介面,然後在不同情況下,去顯示不同的選單功能即可。

比如後臺傳給我們:

{
    "companys": [
        {
            "公司1": [
                {
                    "name": "吃飯",
                    "iconName": "icon_xxx",
                    "typeId": "1"
                }
            ]
        }
    ]
}複製程式碼

這樣我們就很大程式前段就自由了。那我們的難點就變成了:
既然我們是動態的顯示這個選單,拿到這些資料後怎麼來呈現呢
很多人應該做這麼個介面會覺得簡單,但是如果是一個根據數量自動排好的選單介面就有點不知所措了。所以這裡我們的難點就變成了。如果給了我們N個資料,我們要在這個彈框中顯示出N個,那我們的問題也就變成了:能否提供一個自定義的ViewGroup,然後我傳入幾個View物件,可以按照一定的規則幫我自動排布,這樣我們拿到N個資料後,只需要新建相應的View物件,然後新增到這個ViewGroup就行了。

答案當然是能提供。(這波B裝的太累了。喘口氣。)


既然我們要做的是一個自動按照上面圖片顯示排布規則的ViewGroup,系統肯定是沒有自帶的。所以我們就需要自定義一個ViewGroup。

  1. 自定義ViewGroup的第一步:繼承ViewGroup:
public class CircleLayout extends ViewGroup {

    private float mAngleOffset;
    private float mAngleRange;
    private int mInnerRadius;

    public CircleLayout(Context context) {
        super(context);
    }

    public CircleLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CircleLayout, 0, 0);

        try {
            mAngleOffset = a.getFloat(R.styleable.CircleLayout_angleOffset, -90f);
            mAngleRange = a.getFloat(R.styleable.CircleLayout_angleRange, 360f);
            mInnerRadius = a.getDimensionPixelSize(R.styleable.CircleLayout_innerRadius, 80);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            a.recycle();
        }
    }
}複製程式碼

說明下三個引數:

private float mAngleOffset;//擺放子View的角度偏移值
private float mAngleRange;//子VIew可以擺放的角度範圍,比如最多是360度
private int mInnerRadius;//子View距離這個ViewGroup中心點的距離複製程式碼

2.實現onMeasure方法:

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

    final int count = getChildCount();

    int maxHeight = 0;
    int maxWidth = 0;


    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
            maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
        }
    }

    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    int width = resolveSize(maxWidth, widthMeasureSpec);
    int height = resolveSize(maxHeight, heightMeasureSpec);

    if(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST){
        setMeasuredDimension(1000, 1000);
    }else if(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST){
        setMeasuredDimension(1000, height);
    }else if(MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST){
        setMeasuredDimension(width, 1000);
    }else{
        setMeasuredDimension(width, height);
    }
}複製程式碼

3.實現onLayout方法:
這個很重要,我重點也是講解這個onLayout方法,因為如果你繼承ViewGroup,會提示你一定要實現這個方法。而且我們也知道了我們最主要的點就在於怎麼根據傳進來的子View的個數來進行相應的擺放。所以我們直接來看onLayout的具體實現。


onLayout方法:

我們假設我們的自定義ViewGroup是佔滿整個螢幕的,都是match_parent。然後就如下圖所示:

這時候如果我們想擺四個子View(四個的分析起來簡單點),這時候的介面應該是:

這時候大家肯定想,這有什麼規則嗎,完全沒想法啊。。哈哈不要急,看我一步操作,你馬上懂:

我們移動畫布,等價我們讓座標系移動了中間,這時候你是不是恍然大悟,我們只需要按照角度來不就可以了嗎。

我們繼續往下看:

好的。我們可以看到每個子View分到的角度應該是(360 / 4 = 90),而這個子View的中心點又是子View分到的角度的一半:(90/2)。而且這些子View 的中心離原點的距離,都是這個我畫的圓形的半徑。好了所以現在我們就知道了。

我們假設是寬比高小,我們的圓形的半徑就是寬(也就是說圓形的半徑取得是(寬和高中的偏小的值))子View的擺放位置的中心點就是這個圓形的半徑R(在此處也就是viewGroup.Width/2),而這個子View的top值就是(半徑R*sin(相應的角度) - 子View高度/2),子View的left值就是(半徑R*cos(相應的角度) - 子View寬度/2),子View的bottom值就是(半徑R*sin(相應角度) + 子View高度/2),子View的right值就是(半徑R*cos(相應角度) + 子View寬度/2)

還記不記得我們前面有自定義三個屬性,就是:

private float mAngleOffset;//擺放子View的角度偏移值
private float mAngleRange;//子VIew可以擺放的角度範圍,比如最多是360度
private int mInnerRadius;//子View距離這個ViewGroup中心點的距離複製程式碼

那我們再外加上著三個屬性:

  1. mAngleOffset:角度偏移值,就用在原本的子View的中心角度(90/2)處,變成了(90/2 + mAngleOffset),這樣View 就等於轉過來了一定的角度。
  2. mAngleRange:總的角度,我們上面預設是360度,所以每個子View 所佔的角度範圍是(360/4 = 90 ),如果設定了這個值,我們就是(mAngleRange/4)。
  3. mInnerRadius:距離中心的距離,我們本來半徑是(viewGroup.Width /2),現在變為((viewGroup.Width - mInnerRadius) / 2),也就是說離座標系的中間的的距離更近了。子View之間也就更近了。

千萬別忘了。我們前面的討論的前提都是座標系已經移動到了這個螢幕的中間,所以我們最後要子View的X ,Y 都重新加上相應的偏移值,也就是 (x+ width/2),(y + height /2 ),還有就是如果子View的數量是1的話,直接就放在了中心地方,也就是 width/2 和height /2處。

最終的程式碼:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int childs = getChildCount();

    final int width = getWidth();
    final int height = getHeight();

    final float minDimen = width > height ? height : width;
    final float radius = (minDimen - mInnerRadius) / 2f;

    float startAngle = mAngleOffset;

    for (int i = 0; i < childs; i++) {
        final View child = getChildAt(i);

        final LayoutParams lp = child.getLayoutParams();

        final float angle = mAngleRange / childs;

        final float centerAngle = startAngle + angle / 2f;
        int x;
        int y;

        if (childs > 1) {
            x = (int) (radius * Math.cos(Math.toRadians(centerAngle))) + width / 2;
            y = (int) (radius * Math.sin(Math.toRadians(centerAngle))) + height / 2;
        } else {
            x = width / 2;
            y = height / 2;
        }

        final int halfChildWidth = child.getMeasuredWidth() / 2;
        final int halfChildHeight = child.getMeasuredHeight() / 2;

        final int left = lp.width != LayoutParams.MATCH_PARENT ? x - halfChildWidth : 0;
        final int top = lp.height != LayoutParams.MATCH_PARENT ? y - halfChildHeight : 0;
        final int right = lp.width != LayoutParams.MATCH_PARENT ? x + halfChildWidth : width;
        final int bottom = lp.height != LayoutParams.MATCH_PARENT ? y + halfChildHeight : height;

        child.layout(left, top, right, bottom);
        startAngle += angle;

    }
    invalidate();
}複製程式碼

如果哪裡錯了。或者有好的改進,希望大家多指出,謝謝。

附上DEMO:CirclerMenuDialog

相關文章