自定义属性文件 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>
-
attr 代表属性,name 为属性的名称
-
enum 为枚举类型,也就是说该属性有 enum 这些值可选
-
declare 是对属性的声明,使得其可以在 XML 的命名空间中使用
-
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);
}
-
TypedValue.applyDimension()
方法可以得到带单位的尺寸,本例中即得到 100dip -
getTheme().obtainStyledAttributes()
方法可以得到在 XML 文件中配置的属性值 -
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 的大小
widthMeasureSpec
和 heightMeasureSpec
来源于 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);
}