<?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 为属性的名称html
enum 为枚举类型,也就是说该属性有 enum 这些值可选android
declare 是对属性的声明,使得其能够在 XML 的命名空间中使用spring
styleable 是指这个属性能够调用 style 或 theme 来做为 XML 属性的值express
xmlns:app="http://schemas.android.com/apk/res-auto"
在 Android Studio 的 IDE 下,用该代码引入命名空间app
<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>
在布局文件中配置控件的属性ide
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()
方法来回收this
public ArcMenuButton(Context context) { this(context, null); } public ArcMenuButton(Context context, AttributeSet attrs) { this(context, attrs, 0); }
定义缺省构造方法,不然在 Inflate 时会出错。.net
<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>
@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 等。
@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