<?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