PercentLayout原理以及擴展

概述

Percent Support Library 用于使用百分比控制子View在布局里面占用的大小,相比于layout_weight這個屬性具有更高的靈活性,并且margin屬性也支持使用百分比控制,這是layout_weight不具備的,官方只提供了PercentFrameLayout和PercentRelativeLayout,但是我們可以使用PercentLayoutHelper來讓我們的布局也支持百分比的效果。目前支持的屬性有:

  • layout_widthPercent
  • layout_heightPercent
  • layout_marginPercent
  • layout_marginLeftPercent
  • layout_marginTopPercent
  • layout_marginRightPercent
  • layout_marginBottomPercent
  • layout_marginStartPercent
  • layout_marginEndPercent
  • layout_aspectRatio

特別地,layout_aspectRatio是用來表示寬高比例,當我們指定寬或者高的任一邊的長度或者百分比,其就能夠自動地計算出來另外一邊的長度。

使用

在module級別的build.gradle里面的dependencies加上依賴即可:

dependencies {
    com.android.support:percent:23.3.0
}

先來看一下效果圖:

PercentRelativeLayout

直接在布局文件里面聲明即可,也比較好理解,不再累述,下面直接看實現的原理。

<android.support.percent.PercentRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/content"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/view1"
        app:layout_widthPercent="25%"
        app:layout_heightPercent="10%"
        android:background="#333366"/>

    <View
        android:id="@+id/view2"
        android:layout_toRightOf="@id/view1"
        app:layout_widthPercent="25%"
        app:layout_heightPercent="10%"
        android:background="#999933"/>

    <View
        android:id="@+id/view3"
        android:layout_toRightOf="@id/view2"
        app:layout_widthPercent="25%"
        app:layout_heightPercent="10%"
        android:background="#996600"/>

    <View
        android:layout_toRightOf="@id/view3"
        app:layout_widthPercent="25%"
        app:layout_heightPercent="10%"
        android:background="#333333"/>

    <View
        app:layout_aspectRatio="578%"
        app:layout_widthPercent="100%"
        app:layout_marginTopPercent="10%"
        android:background="#669999" />


</android.support.percent.PercentRelativeLayout>

原理

由于上面的例子是PercentRelativeLayout,所以我們使用其來講解,其實PercentFrameLayout和PercentRelativeLayout里面的代碼幾乎一樣。

系統初始化布局都是通過LayoutInflater來實現的,比如在setContentView里面。LayoutParams在平時使用還是比較多的,其作用就是讓父View決定如何擺放自己以及自己的寬高。當我們把child view寫到布局里面,那么child view的LayoutParams由ViewGroup的generateDefaultLayoutParams來設置。下面就是LayoutInflater的為子View賦值LayoutParams的關鍵代碼:

void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
    boolean finishInflate, boolean inheritContext) {
    //...
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
        parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        //...
        final View view = createViewFromTag(parent, name, attrs, inheritContext);
        final ViewGroup viewGroup = (ViewGroup) parent;
        final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
        rInflate(parser, view, attrs, true, true);
        viewGroup.addView(view, params);
    //...
}

PercentRelativeLayout所以就實現了generateLayoutParams的方法,并且返回的是繼承的RelativeLayout.LayoutParams,這樣就保留了RelativeLayout原來屬性。并且generateLayoutParams的方法參數是AttributeSet,里面就包含了我們聲明的PercentLayout的屬性值,例如layout_widthPercent等等。

PercentRelativeLayout.LayoutParams在構造方法就通過PercentLayoutHelper對AttributeSet進行解析,解析的結果保存在自定義的數據結構PercentLayoutHelper.PercentLayoutInfo,里面包括了在概述里面說的所有屬性。

private PercentLayoutHelper.PercentLayoutInfo mPercentLayoutInfo;

public LayoutParams(Context c, AttributeSet attrs) {
    super(c, attrs);
    mPercentLayoutInfo = PercentLayoutHelper.getPercentLayoutInfo(c, attrs);
}

另外,我們知道所有的LayoutParams都是繼承ViewGoup.LayoutParams,里面有個方法是用來初始化View兩個layout_width,layout_height:

protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
    width = a.getLayoutDimension(widthAttr, "layout_width");
    height = a.getLayoutDimension(heightAttr, "layout_height");
}

如果我們沒有在布局文件里面聲明這兩個屬性,那么在LayoutInflater初始化的就會拋UnsupportedOperationException。由于使用了百分比的屬性,所以這個屬性就可以不需要,為了讓其不拋異常,必須重寫這個方法。

PercentLayoutHelper#fetchWidthAndHeight就是讓其在沒有值的情況下讓LayoutParams的width和height的值都為0。

@Override
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
    PercentLayoutHelper.fetchWidthAndHeight(this, a, widthAttr, heightAttr);
}

初始化布局的時候已經把所有需要的數據都保持在了PercentLayoutInfo里面,接下來就到了我們熟悉的三大流程了:onMeasure->onLayout->onDraw,由于是ViewGroup,所以只需要關注前面兩個即可。先來看onMeasure:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    mHelper.adjustChildren(widthMeasureSpec, heightMeasureSpec);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    if (mHelper.handleMeasuredStateTooSmall()) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

adjustChildren的主要工作就是遍歷所有的child view,通過child view的PercentLayoutHelper.LayoutParams的寬高百分比轉換為實際的占用的像素寬高。并保存在對應child view的LayoutParams里,然后再調用RelativeLayout原有的onMeasure,就可以實現寬高的百分比轉換。

我們在前面讀書筆記中View的工作原理measure的過程里面有提到,有時候我們在測量View的時候,如果父View最大能夠給我們的空間小于我們需要的空間,就會給測量結果的高兩位加上相應的狀態表示MEASURED_STATE_TOO_SMALL。

如果出現了這種情況,并且為layout_width和layout_height設置了wrap_content,就需要調用handleMeasuredStateTooSmall來處理,將寬或者高重新按照wrap_content的屬性來測量。

然后就到了onLayout的階段,基本什么也沒做。如果在child view里面設置了layout_width,layout_height等屬性,那么在onMeasure階段就會調用adjustChildren將他們都保存起來,等onLayout結束之后再把他們給還原回去。

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    mHelper.restoreOriginalParams();
}

擴展

通過前面的分析,我們可以很容易地將我們現有的組件通過PercentLayoutHelper這個類讓我們現有的組件支持百分比控制child view的寬高,比如LinearLayout。代碼幾乎都長得一樣,可以具體看Github上面的實現:)

根據上面的原理分析,具體的實現步驟:

1.繼承布局原有的LayoutParams,并實現PercentLayoutHelper.PercentLayoutParams接口并在構造方法里面調用getPercentLayoutInfo(Context, AttributeSet)解析layout_widthPercent等參數。

public static class LayoutParams extends LinearLayout.LayoutParams implements PercentLayoutHelper.PercentLayoutParams {
    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        mPercentLayoutInfo = PercentLayoutHelper.getPercentLayoutInfo(c, attrs);
    }
}

2.重寫在LayoutParams的setBaseAttributes(TypedArray, int, int) 方法,里面就加這一句代碼:

@Override
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
    PercentLayoutHelper.fetchWidthAndHeight(this, a, widthAttr, heightAttr);
}

3.重寫布局的generateLayoutParams(AttributeSet)方法,新構造我們實現了LayoutParams。

4.在onMeasure(int, int)方法里面調用mHelper.adjustChildren進行百分比轉換:

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     mHelper.adjustChildren(widthMeasureSpec, heightMeasureSpec);
     super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     if (mHelper.handleMeasuredStateTooSmall()) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     }
 }

5.在onLayout方法里面調用mHelper.restoreOriginalParams()恢復默認的LayoutParams參數:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mHelper.restoreOriginalParams();
}

參考資料:

1.PercentLayoutHelper

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容