在開發(fā)過程中,常常會(huì)遇到需要固定寬高比例展現(xiàn)一個(gè)布局,最常見的就是實(shí)現(xiàn)正方形布局。在這里,簡單地通過繼承RelativeLayout的方式,來實(shí)現(xiàn)任意寬高比的RectangleLayout這個(gè)ViewGroup。
實(shí)現(xiàn)思路
- 創(chuàng)建RelativeLayout的子類
- 自定義屬性
- 復(fù)寫onMeasure()方法
具體實(shí)現(xiàn)
創(chuàng)建一個(gè)類,繼承自RelativeLayout。
values資源文件夾中,新建attrs.xml文件。在resources節(jié)點(diǎn)下,申明我們需要的兩個(gè)自定義屬性。
<declare-styleable name="RectangleLayout">
<!--寬度占比-->
<attr name="width_weight" format="integer"/>
<!--高度占比-->
<attr name="height_weight" format="integer"/>
</declare-styleable>
簡單改寫一下三個(gè)構(gòu)造器,通過TypedArray獲得自定義屬性的值。
//寬度占比
private int width_weight = 1;
//高度占比
private int height_weight = 1;
public RectangleLayout(Context context) {
this(context, null);
}
public RectangleLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RectangleLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.RectangleLayout, defStyleAttr, 0);
for (int i = 0; i < typedArray.getIndexCount(); i++) {
int attr = typedArray.getIndex(i);
switch (attr) {
case R.styleable.RectangleLayout_width_weight:
//默認(rèn)值為1
width_weight = typedArray.getInt(attr, 1);
break;
case R.styleable.RectangleLayout_height_weight:
//默認(rèn)值為1
height_weight = typedArray.getInt(attr, 1);
break;
}
}
}
3.要實(shí)現(xiàn)寬高定比,最重要的就是復(fù)寫onMeasure方法。廢話不多說,先貼代碼。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//獲取父容器允許的寬高
int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//獲取測(cè)量的寬高
int mWidth = getMeasuredWidth();
int mHeight = getMeasuredHeight();
//最終設(shè)定的寬高
int width = 0;
int height = 0;
//根據(jù)MeasureSpec模式的不同,寬高取值不同
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//當(dāng)都為wrap時(shí),寬高取最大值
if (mWidth * height_weight >= mHeight * width_weight) {
width = mWidth;
height = width * height_weight / width_weight;
} else {
height = mHeight;
width = height * width_weight / height_weight;
}
} else if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
//當(dāng)都為固定值或match時(shí),寬高取最小值,以保證不會(huì)越過設(shè)定的最大范圍
if (mWidth * height_weight <= mHeight * width_weight) {
width = mWidth;
height = width * height_weight / width_weight;
} else {
height = mHeight;
width = height * width_weight / height_weight;
}
} else if (widthMode == MeasureSpec.EXACTLY) {
//當(dāng)一項(xiàng)設(shè)為固定值或match時(shí),以這條為標(biāo)準(zhǔn)
width = mWidth;
height = width * height_weight / width_weight;
} else if (heightMode == MeasureSpec.EXACTLY) {
//當(dāng)一項(xiàng)設(shè)為固定值或match時(shí),以這條為標(biāo)準(zhǔn)
height = mHeight;
width = height * width_weight / height_weight;
}
//最終和父容器允許的寬高比較,最大不超過父容器的約束
if (maxWidth < width) {
width = maxWidth;
height = width * height_weight / width_weight;
}
if (maxHeight < height) {
height = maxHeight;
width = height * width_weight / height_weight;
}
//將最終的寬高設(shè)定為容器的寬高
setMeasuredDimension(width, height);
}
閱讀RelativeLayout的源碼后,可以發(fā)現(xiàn),在onMeasure方法中,主要做了如下幾件事:
- 把內(nèi)部子View進(jìn)行橫向和縱向的排序
- 初始化一些變量值,來記錄一些狀態(tài)
- 遍歷水平關(guān)系的View
- 遍歷垂直關(guān)系的View
- baseline的計(jì)算
- 高度和寬度的修正
可以發(fā)現(xiàn),最復(fù)雜的子View位置的計(jì)算,RelativeLayout已經(jīng)幫我們做好了,我們要做的只是最后一步,寬度和高度的修正(按一定的比例給值),這也是為什么繼承一個(gè)現(xiàn)有ViewGroup的原因。下面看代碼。
關(guān)于MeasureSpec和getMeasuredWidth()等不太清楚的同學(xué)可以自行百度學(xué)習(xí)一下,這里就不做過多的說明了。首先獲取我們的ViewGroup能夠占用的最大寬高,由父容器和自身寬高的設(shè)定有關(guān),先記錄一下,備用。下面,拋開父容器不管,單憑自身的設(shè)定以及子View測(cè)量出來的寬高,來計(jì)算寬高的值。
在計(jì)算過程中,我們始終遵循如下幾條原則(由于原則都是聽不懂的鬼話,我都舉個(gè)栗子來加以說明):
- 當(dāng)ViewGroup寬高的模式都設(shè)為wrap_content時(shí),看子View占據(jù)的最大寬高,盡量滿足子View。
例如,布局中子View橫向縱向的寬高加上margin等值,共需要100dp x 120dp。而寬高比設(shè)定為2:1,那么最終寬高就應(yīng)該是240dp x 120dp。 - 當(dāng)ViewGroup寬高的模式都為EXACTLY時(shí),以該ViewGroup為準(zhǔn),子View不夠時(shí),空著;子View太大時(shí),顯示一部分。由于ViewGroup寬高固定,但要滿足一定的寬高比例,只能取在這個(gè)范圍內(nèi),最大的那個(gè)矩形。
例如ViewGroup寬高設(shè)定為50dp x 40dp,寬高比還是2:1,而在50 x 40這個(gè)矩形中,最大的矩形應(yīng)該是50 x 25的,那么最終獲得的寬高不管子View大還是小,多還是少,寬高就是50dp x 25dp,之前設(shè)置的高40dp無效。 - 當(dāng)ViewGroup寬高的模式只有一個(gè)為EXACTLY時(shí),以這個(gè)為標(biāo)準(zhǔn)。這個(gè)就不舉例子了。
- 最大寬高不能超過父容器約束的寬高。若按照以上三條原則計(jì)算出寬高為50 x 50,而父容器只能允許40 x 40,那么最終寬高也只能是40 x 40。
代碼中,乘法等式比較多,可能一眼看不明白,移項(xiàng)后則為 **寬/高?寬比/高比 **。邏輯并不復(fù)雜,僅僅只是寬高比的比較和修正,就能達(dá)到預(yù)期的目的。
- 下面演示一下最終效果。(記得在根布局中加入 xmlns:tools="http://schemas.android.com/tools")
xml中:
<com.ugly.doggie.RectangleLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginTop="10dp"
android:background="#0f0"
app:width_weight="2"
app:height_weight="3">
<TextView
android:id="@+id/tv1"
android:layout_width="100dp"
android:layout_height="120dp"
android:background="#f00"/>
<TextView
android:id="@+id/tv2"
android:layout_toRightOf="@+id/tv1"
android:layout_width="20dp"
android:layout_height="30dp"
android:background="#00f"/>
<TextView android:id="@+id/tv3"
android:layout_below="@+id/tv1"
android:layout_width="30dp"
android:layout_height="40dp"
android:background="#ff0"/>
</com.ugly.doggie.RectangleLayout>
效果圖如下:

最終獲得寬為120dp,高為180dp。
看到這里也許你會(huì)說,這樣設(shè)置完全沒必要啊,因?yàn)樗械膶捀咦约憾寄芩愠鰜砹?,手?dòng)設(shè)置一下不就完了嗎。但是你們?cè)陂_發(fā)中有沒有碰到過這么一種需求:需要三個(gè)正方形的布局,橫向占滿整個(gè)屏幕。寬高難道要在代碼中計(jì)算然后重新設(shè)置LayoutParams嗎?太復(fù)雜了吧?,F(xiàn)在外面只要這么寫:(自定義屬性不寫,默認(rèn)為1:1,即正方形)
xml代碼:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="horizontal">
<com.ugly.doggie.RectangleLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:background="#f00">
</com.ugly.doggie.RectangleLayout>
<com.ugly.doggie.RectangleLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:background="#0f0">
</com.ugly.doggie.RectangleLayout>
<com.ugly.doggie.RectangleLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:background="#00f">
</com.ugly.doggie.RectangleLayout>
</LinearLayout>
效果圖如下:

。。。國旗?Σ( ° △ °|||)︴
好了算是基本實(shí)現(xiàn)了。打完收工!