自定义属性文件 attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <attr name="position">
        <enum name="left_top" value="0"/>
        <enum name="left_bottom" value="1"/>
        <enum name="right_top" value="2"/>
        <enum name="right_bottom" value="3"/>
    </attr>

    <attr name="radius" format="dimension"/>

    <declare-styleable name="ArcMenuButton">
        <attr name="position"/>
        <attr name="radius"/>
    </declare-styleable>

</resources>
  1. attr 代表属性,name 为属性的名称

  2. enum 为枚举类型,也就是说该属性有 enum 这些值可选

  3. declare 是对属性的声明,使得其可以在 XML 的命名空间中使用

  4. styleable 是指这个属性可以调用 style 或 theme 来作为 XML 属性的值

在布局文件中使用

xmlns:app="http://schemas.android.com/apk/res-auto"

在 Android Studio 的 IDE 下,用该代码引入命名空间

<cn.koreylee.arcmenubutton.ArcMenuButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:position="right_bottom"
    app:radius="100dp">
    
</cn.koreylee.arcmenubutton.ArcMenuButton>

在布局文件中配置控件的属性

在自定义控件中读取

public ArcMenuButton(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    float defExpandedRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100,
            getResources().getDisplayMetrics());
    this.mExpandedRadius = defExpandedRadius;
    TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs,
            R.styleable.ArcMenuButton, defStyleAttr, 0);
    int pos = typedArray.getInt(R.styleable.ArcMenuButton_position, 3);
    switch (pos) {
        case POS_LEFT_TOP:
            mMenuButtonPosition = Position.LEFT_TOP;
            break;
        case POS_LEFT_BOTTOM:
            mMenuButtonPosition = Position.LEFT_BOTTOM;
            break;
        case POS_RIGHT_TOP:
            mMenuButtonPosition = Position.RIGHT_TOP;
            break;
        case POS_RIGHT_BOTTOM:
            mMenuButtonPosition = Position.RIGHT_BOTTOM;
            break;
    }
    mExpandedRadius = typedArray.getDimension(R.styleable.ArcMenuButton_radius, defExpandedRadius);
    // Be sure to call recycle() when you are done with the array.
    typedArray.recycle();
    Log.d(TAG, "ArcMenuButton: " + "Position = " + mMenuButtonPosition + ", "
            + "Radius = " + mExpandedRadius);
}
  1. TypedValue.applyDimension() 方法可以得到带单位的尺寸,本例中即得到 100dip

  2. getTheme().obtainStyledAttributes() 方法可以得到在 XML 文件中配置的属性值

  3. TypedArray 在使用完后需要调用 recycle() 方法来回收

public ArcMenuButton(Context context) {
    this(context, null);
}

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

定义缺省构造方法,否则在 Inflate 时会出错。

完成在 ViewGroup 中的布局

    <cn.koreylee.arcmenubutton.ArcMenuButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:position="left_top"
        app:radius="125dp">

        <ImageView
            android:id="@+id/iv_center_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:src="@drawable/ic_control_point_black_24dp"/>

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_brightness_5_black_24dp"
            android:tag="1"/>

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_brightness_5_black_24dp"
            android:tag="2"/>

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_brightness_5_black_24dp"
            android:tag="3"/>

    </cn.koreylee.arcmenubutton.ArcMenuButton>

onMeasure() 方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

onMeasure() 方法用来确定 View 的大小
widthMeasureSpecheightMeasureSpec 来源于 ViewGroup 的 layout_width, layout_height 等属性,当然也会受到其他属性的影响,例如 Margin, Padding, weight 等。

onLayout() 方法

@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
    int count = getChildCount();
    // If changed.
    if (b) {
        layoutCenterButton();
        // From '1' to 'count - 1'
        for (int j = 1; j < count; j++) {
            View childView = getChildAt(j);
            // Invisible at first.
            childView.setVisibility(View.GONE);
            // Use left top as position to set first.
            int childTop = (int) (mExpandedRadius *
                    Math.cos(Math.PI / 2 / (count - 2) * (j - 1)));
            int childLeft = (int) (mExpandedRadius *
                    Math.sin(Math.PI / 2 / (count - 2) * (j - 1)));
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();
            // If position is bottom, make adjustment.
            if (mMenuButtonPosition == Position.RIGHT_BOTTOM ||
                    mMenuButtonPosition == Position.LEFT_BOTTOM) {
                childTop = getMeasuredHeight() - childTop - childHeight;
            }
            // If position is right, make adjustment.
            if (mMenuButtonPosition == Position.RIGHT_BOTTOM ||
                    mMenuButtonPosition == Position.RIGHT_TOP) {
                childLeft = getMeasuredWidth() - childLeft - childWidth;
            }
            childView.layout(childLeft, childTop, childLeft + childWidth,
                    childTop + childHeight);
        }
    }
}

我们来分段分析一下

for (int j = 1; j < count; j++)

第 0 个元素是中心的按钮,所以从 1 开始。

View childView = getChildAt(j);
// Invisible at first.
childView.setVisibility(View.GONE);
// Use left top as position to set first.
int childTop = (int) (mExpandedRadius *
        Math.cos(Math.PI / 2 / (count - 2) * (j - 1)));
int childLeft = (int) (mExpandedRadius *
        Math.sin(Math.PI / 2 / (count - 2) * (j - 1)));
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();

首先设置为不可见的 GONE,再通过三角函数得出横纵坐标。

// If position is bottom, make adjustment.
if (mMenuButtonPosition == Position.RIGHT_BOTTOM ||
        mMenuButtonPosition == Position.LEFT_BOTTOM) {
    childTop = getMeasuredHeight() - childTop - childHeight;
}
// If position is right, make adjustment.
if (mMenuButtonPosition == Position.RIGHT_BOTTOM ||
        mMenuButtonPosition == Position.RIGHT_TOP) {
    childLeft = getMeasuredWidth() - childLeft - childWidth;
}

之前的计算是以在左上角为例的,那么在其他位置需要做相应的补偿。

childView.layout(childLeft, childTop, childLeft + childWidth,
                    childTop + childHeight);

最后 layout 子视图

中心按钮方法类似,通过 layoutCenterButton() 方法来配置即可。

private void layoutCenterButton() {

    mCenterButton = getChildAt(0);
    mCenterButton.setOnClickListener(this);

    int top = 0;
    int left = 0;
    int centerButtonWidth = mCenterButton.getMeasuredWidth();
    int centerButtonHeight = mCenterButton.getMeasuredHeight();
    switch (mMenuButtonPosition) {
        case LEFT_TOP:
            break;
        case LEFT_BOTTOM:
            top = getMeasuredHeight() - centerButtonHeight;
            break;
        case RIGHT_TOP:
            left = getMeasuredWidth() - centerButtonWidth;
            break;
        case RIGHT_BOTTOM:
            top = getMeasuredHeight() - centerButtonHeight;
            left = getMeasuredWidth() - centerButtonWidth;
            break;
    }
    mCenterButton.layout(left, top, left + centerButtonWidth, top + centerButtonHeight);
}

getWidth() 和 getMeasuredWidth() 有什么区别呢?
我们来看下 Stack Overflow 上的解释就明白了。
Difference between getheight() and getmeasuredheight() - Stack Overflow

View.getMeasuredWidth() and View.getMeasuredHeight() represents the dimensions the view wants to be, before all views in the layout are calculated and laid in the screen.

After View.onMeasure(int, int) and View.onLayout(boolean, int, int, int, int), views measurements could be change to accommodate everything. These (possible) new values are then accessible through View#getWidth() and View#getHeight().

From the View Class Reference

The size of a view is expressed with a width and a height. A view actually possess two pairs of width and height values.

The first pair is known as measured width and measured height. These dimensions define how big a view wants to be within its parent (see Layout for more details.) The measured dimensions can be obtained by calling getMeasuredWidth() and getMeasuredHeight().

The second pair is simply known as width and height, or sometimes drawing width and drawing height. These dimensions define the actual size of the view on screen, at drawing time and after layout. These values may, but do not have to, be different from the measured width and height. The width and height can be obtained by calling getWidth() and getHeight().


Reference:
Android 实现卫星菜单 - 慕课网
ViewGroup 的概念和理解 - CSDN
Android中 View 自定义 XML 属性详解以及 R.attr 与 R.styleable 的区别 - CSDN
Android 一张图理解 getWidth 和 getMeasuredWidth
Difference between getheight() and getmeasuredheight() - Stack Overflow


KoreyLee
27 声望6 粉丝

Those who were seen dancing were thought to be insane by those who could not hear the music.