自定義寬高比的RectangleLayout的簡單實(shí)現(xiàn)

在開發(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)

  1. 創(chuàng)建一個(gè)類,繼承自RelativeLayout。

  2. 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方法中,主要做了如下幾件事:

  1. 把內(nèi)部子View進(jìn)行橫向和縱向的排序
  2. 初始化一些變量值,來記錄一些狀態(tài)
  3. 遍歷水平關(guān)系的View
  4. 遍歷垂直關(guān)系的View
  5. baseline的計(jì)算
  6. 高度和寬度的修正

可以發(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è)栗子來加以說明):

  1. 當(dāng)ViewGroup寬高的模式都設(shè)為wrap_content時(shí),看子View占據(jù)的最大寬高,盡量滿足子View。
    例如,布局中子View橫向縱向的寬高加上margin等值,共需要100dp x 120dp。而寬高比設(shè)定為2:1,那么最終寬高就應(yīng)該是240dp x 120dp。
  2. 當(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無效。
  3. 當(dāng)ViewGroup寬高的模式只有一個(gè)為EXACTLY時(shí),以這個(gè)為標(biāo)準(zhǔn)。這個(gè)就不舉例子了。
  4. 最大寬高不能超過父容器約束的寬高。若按照以上三條原則計(jì)算出寬高為50 x 50,而父容器只能允許40 x 40,那么最終寬高也只能是40 x 40。

代碼中,乘法等式比較多,可能一眼看不明白,移項(xiàng)后則為 **寬/高?寬比/高比 **。邏輯并不復(fù)雜,僅僅只是寬高比的比較和修正,就能達(dá)到預(yù)期的目的。

  1. 下面演示一下最終效果。(記得在根布局中加入 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)了。打完收工!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容