今天分享一些layout布局書寫中的一些技巧,希望看過之后你也一樣可以寫出性價比高的布局。我個人的目標是用最少的View寫出一樣效果的布局。因為我相信View的數量減少伴隨着的就是層級的減少。從而達到結構清晰,渲染速度快的效果。順着這個邏輯,我將優化分為重用、合並、按需載入。
重用
< include/>
< include>標簽可以在一個布局中引入另外一個布局,這個的好處顯而易見。類似於我們經常用到的工具類,隨用隨調。便於統一修改使用。
舉例說明:首先寫一個公共的布局title_bar.xml,app中常用的標題欄。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:background="@color/background" android:layout_height="48dp"> <ImageView android:layout_width="wrap_content" android:layout_height="match_parent" android:paddingLeft="15dp" android:paddingRight="15dp" android:src="@drawable/icon_back"/> <TextView tools:text="標題" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:textSize="18sp" android:textColor="@color/white" /> <TextView tools:text="確定" android:layout_width="wrap_content" android:gravity="center" android:layout_height="match_parent" android:layout_alignParentRight="true" android:paddingLeft="15dp" android:paddingRight="15dp" android:textSize="16sp" android:textColor="@color/white" /> </RelativeLayout>
預覽:
下來activity_main.xml調用它:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <include layout="@layout/title_bar"/> </RelativeLayout>
運行后效果和預覽一樣一樣的。當然我們也可以在< include>標簽當中重新設置寬高等layout屬性。
合並
減少嵌套
首先我們心中要有一個大原則:盡量保持布局層級的扁平化。在這個大原則下我們要知道:
-
在不影響層級深度的情況下,使用LinearLayout而不是RelativeLayout。因為RelativeLayout會讓子View調用2次onMeasure,LinearLayout 在有weight時,才會讓子View調用2次onMeasure。Measure的耗時越長那么繪制效率就低。
-
如果非要是嵌套,那么盡量避免RelativeLayout嵌套RelativeLayout。這簡直就是惡性循環,喪心病狂。
實現方法就不細說了,大家都是明白人。
< merge/>
< merge/>主要用來去除不必要的FrameLayout。它的使用最理想的情況就是你的根布局是FrameLayout,同時沒有使用background等屬性。這時可以直接替換。因為我們布局外層就是FrameLayout,直接“合並”。
舉例說明:比如上面用到的activity_main.xml文件,我們通過View Hierarchy工具看一下,如圖:
可以看到,最外層是FrameLayout,下來我們修改一下。
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <include layout="@layout/title_bar"/> </merge>
再次查看:
很明顯少了一層RelativeLayout,當然運行效果是一樣的。當然如果我們不需要title_bar.xml中的綠色背景,那么可以這樣修改。
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="wrap_content" android:layout_height="48dp" android:paddingLeft="15dp" android:paddingRight="15dp" android:src="@drawable/icon_back_1"/> <TextView android:layout_gravity="center_horizontal" android:text="標題" android:gravity="center" android:layout_width="wrap_content" android:layout_height="48dp" android:layout_centerInParent="true" android:textSize="18sp" android:textColor="@color/black" /> <TextView android:text="確定" android:layout_gravity="right" android:layout_width="wrap_content" android:gravity="center" android:layout_height="48dp" android:layout_alignParentRight="true" android:paddingLeft="15dp" android:paddingRight="15dp" android:textSize="16sp" android:textColor="@color/black" /> </merge>
運行效果:
運行查看層級,如下圖:
結果很明顯。
用TextView同時顯示圖片和文字
這個我就不細說了,舉一個我們項目中的一個例子,代碼一看便知。
首先要完成的效果是如下圖:
這種效果很常見,一般實現方法是這樣。(貌似沒人這樣寫吧,哈哈)
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:orientation="horizontal" android:background="@color/white" android:layout_width="match_parent" android:layout_height="50dp"> <ImageView android:layout_marginLeft="10dp" android:layout_width="wrap_content" android:src="@drawable/icon_1" android:layout_height="match_parent" /> <TextView android:paddingLeft="10dp" android:paddingRight="10dp" android:textSize="16sp" android:text="我的卡券" android:gravity="center_vertical" android:layout_width="0dp" android:layout_weight="1" android:layout_height="match_parent" /> <ImageView android:layout_marginRight="10dp" android:src="@drawable/icon_4" android:layout_width="wrap_content" android:layout_height="match_parent"/> </LinearLayout> </LinearLayout>
效果圖:
那么我們優化一下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:drawableLeft="@drawable/icon_1" android:drawableRight="@drawable/icon_4" android:drawablePadding="10dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:textSize="16sp" android:text="我的卡券" android:background="@color/white" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="50dp" /> </LinearLayout>
你沒有看錯,少了兩個ImageView和去除嵌套LinearLayout。效果不用說一樣一樣的。當然EditView等也一樣的,還有屬性drawableBottom和drawableTop供你使用。同時利用代碼setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom)
可以讓我們動態去設置圖片。
使用TextView的行間距
先上我們需要實現的效果圖:
效果很簡單,實現代碼:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="100dp" android:background="@color/white" android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"> <ImageView android:padding="25dp" android:src="@drawable/kd_1" android:layout_width="100dp" android:layout_height="100dp"/> <LinearLayout android:layout_width="match_parent" android:orientation="vertical" android:layout_height="100dp"> <TextView tools:text="攬件方式:上門取件" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="25dp"/> <TextView tools:text="快遞公司:順豐快遞" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="25dp"/> <TextView tools:text="預約時間:9月6日 立即取件" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="25dp"/> <TextView tools:text="快遞費用:等待稱重確定價格" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="25dp"/> </LinearLayout> </LinearLayout>
這里我偷懶了多嵌套了一層LinearLayout,但。。。這不重要,我先直接修改。
優化后代碼:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="100dp" android:background="@color/white" android:layout_width="match_parent"> <ImageView android:padding="25dp" android:src="@drawable/kd_1" android:layout_width="100dp" android:layout_height="match_parent"/> <TextView android:textSize="14dp" android:lineSpacingExtra="8dp" android:gravity="center_vertical" android:text="攬件方式:上門取件\n快遞公司:順豐快遞\n預約時間:9月6日 立即取件\n快遞費用:等待稱重確定價格" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
老規矩,效果一樣一樣的。可以看到我們僅僅利用android:lineSpacingExtra="8dp"
這一行代碼就省去了3個TextView,如果行數更多呢?是不是方便多了。
其中:lineSpacingExtra
屬性代表的是行間距,他默認是0,是一個絕對高度值。同時還有lineSpacingMultiplier
屬性,它代表行間距倍數,默認為1.0f,是一個相對高度值。我們來使用一下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="100dp" android:background="@color/white" android:layout_width="match_parent"> <ImageView android:padding="25dp" android:src="@drawable/kd_1" android:layout_width="100dp" android:layout_height="100dp"/> <TextView android:textSize="14dp" android:lineSpacingMultiplier="1.3" android:gravity="center_vertical" android:text="攬件方式:上門取件\n快遞公司:順豐快遞\n預約時間:9月6日 立即取件\n快遞費用:等待稱重確定價格" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
當然了這兩條屬性可以同時使用,查看源碼可以知道,他們的高度計算規則為mTextPaint.getFontMetricsInt(null) * 行間距倍數 + 行間距。
使用Spannable或Html.fromHtml
這里也是舉例說明,比如下圖效果:
如果實現上圖紅框中的效果,笨辦法就是寫三個TextView,“¥”,“價格”,“門市價”分別實現,其實用一個TextVIew就可以實現,類似如下代碼:
String text = String.format("¥%1$s 門市價:¥%2$s", 18.6, 22); int z = text.lastIndexOf("門"); SpannableStringBuilder style = new SpannableStringBuilder(text); style.setSpan(new AbsoluteSizeSpan(DisplayUtil.dip2px(mContext,14)), 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); //字號 style.setSpan(new ForegroundColorSpan(Color.parseColor("#afafaf")), z, text.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE); //顏色 style.setSpan(new AbsoluteSizeSpan(DisplayUtil.dip2px(mContext,14)), z, text.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE); //字號 tv.setText(style);
同樣Html.fromHtml也可以實現。這樣不就減少了兩個TextView了。
按需載入
ViewStub
在開發中經常會遇到這樣的情況,會在程序運行時動態根據條件來決定顯示哪個View或某個布局。那么通常做法就是把用到的View都寫在布局中,然后在代碼中動態的更改它的可見性。但是它的這樣仍然會創建View,會影響性能。
這時就可以用到ViewStub了,ViewStub是一個輕量級的View,不占布局位置,占用資源非常小。
例子:比如我們請求網絡加載列表,如果網絡異常或者加載失敗我們可以顯示一個提示View,上面可以點擊重新加載。當然一直沒有錯誤時,我們就不顯示。
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > …… <ViewStub android:layout_gravity="center" android:id="@+id/hint_view" android:layout_width="match_parent" android:inflatedId="@+id/hint_view" android:layout_height="wrap_content" android:layout="@layout/hint_view"/> </merge>
hint_view.xml就是這個提示View,可以根據情況自己寫。
用法:
private View hintView; if (網絡異常。。。) { if (hintView == null) { ViewStub viewStub = (ViewStub)this.findViewById(R.id.hint_view); hintView = viewStub.inflate(); TextView textView = (TextView) hintView.findViewById(R.id.tv); textView.setText("網絡異常!"); } hintView.setVisibility(View.VISIBLE); }else{ if (hintView != null) { hintView.setVisibility(View.GONE); } }
用法很簡單,記得一旦ViewStub可見或是被inflate了,ViewStub就不存在了,取而代之的是被inflate的Layout。所以它也被稱做惰性控件。
其他小技巧
用LinearLayout自帶的分割線
還記得上文用TextView同時顯示圖片和文字中的例子嗎?我們可以看到每個條目之間都是有一根分隔線的,那么怎么實現呢?別人我不知道,反正我原來是用一個View設置高度實現的。相信一定有人和我一樣。
那么老辦法我就不演示了,直接上代碼:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:divider="@drawable/divider" android:showDividers="middle"> <TextView android:drawableLeft="@drawable/icon_1" android:drawableRight="@drawable/icon_4" android:drawablePadding="10dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:textSize="16sp" android:text="我的卡券" android:background="@color/white" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="50dp" /> <TextView android:drawableLeft="@drawable/icon_2" android:drawableRight="@drawable/icon_4" android:drawablePadding="10dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:textSize="16sp" android:text="地址管理" android:background="@color/white" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="50dp" /> <TextView android:drawableLeft="@drawable/icon_3" android:drawableRight="@drawable/icon_4" android:drawablePadding="10dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:textSize="16sp" android:text="檢查更新" android:background="@color/white" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="50dp" /> </LinearLayout>
效果圖:
實現的核心部分其實是LinearLayout的這兩行。
android:divider="@drawable/divider" android:showDividers="middle"
其中divider.xml是分隔線樣式。
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <size android:width="1dp" android:height="1dp"/> <solid android:color="#e1e1e1"/> </shape>
showDividers 是分隔線的顯示位置,beginning、middle、end分別代表顯示在開始位置,中間,末尾。
還有dividerPadding屬性這里沒有用到,意思很明確給divider添加padding。感興趣可以試試。
Space控件
還是接着上面的例子,如果要給條目中間添加間距,怎么實現呢?當然也很簡單,比如添加一個高10dp的View,或者使用android:layout_marginTop="10dp"
等方法。但是增加View違背了我們的初衷,並且影響性能。使用過多的margin其實會影響代碼的可讀性。
這時你就可以使用Space,他是一個輕量級的。我們可以看下源碼:
/** * Space is a lightweight View subclass that may be used to create gaps between components * in general purpose layouts. */ public final class Space extends View { /** * {@inheritDoc} */ public Space(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); if (getVisibility() == VISIBLE) { setVisibility(INVISIBLE); } } /** * {@inheritDoc} */ public Space(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } /** * {@inheritDoc} */ public Space(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * {@inheritDoc} */ public Space(Context context) { //noinspection NullableProblems this(context, null); } /** * Draw nothing. * * @param canvas an unused parameter. */ @Override public void draw(Canvas canvas) { } /** * Compare to: {@link View#getDefaultSize(int, int)} * If mode is AT_MOST, return the child size instead of the parent size * (unless it is too big). */ private static int getDefaultSize2(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: result = Math.min(size, specSize); break; case MeasureSpec.EXACTLY: result = specSize; break; } return result; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension( getDefaultSize2(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize2(getSuggestedMinimumHeight(), heightMeasureSpec)); } }
可以看到在draw方法沒有繪制任何東西,那么性能也就幾乎沒有影響。
實現代碼與效果:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:divider="@drawable/divider" android:showDividers="middle|beginning|end"> <TextView android:drawableLeft="@drawable/icon_1" android:drawableRight="@drawable/icon_4" android:drawablePadding="10dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:textSize="16sp" android:text="我的卡券" android:background="@color/white" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="50dp" /> <TextView android:drawableLeft="@drawable/icon_2" android:drawableRight="@drawable/icon_4" android:drawablePadding="10dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:textSize="16sp" android:text="地址管理" android:background="@color/white" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="50dp" /> <Space android:layout_width="match_parent" android:layout_height="15dp"/> <TextView android:drawableLeft="@drawable/icon_3" android:drawableRight="@drawable/icon_4" android:drawablePadding="10dp" android:paddingLeft="10dp" android:paddingRight="10dp" android:textSize="16sp" android:text="檢查更新" android:background="@color/white" android:gravity="center_vertical" android:layout_width="match_parent" android:layout_height="50dp" /> </LinearLayout>
防止過度繪制
這個完全可以看鴻洋大神這篇 《Android UI性能優化實戰 識別繪制中的性能問題》:http://blog.csdn.net/lmj623565791/article/details/45556391
參考
總結
最后想想如果沒有用這些技巧,我們要寫多少代碼,多少View?效果是不是杠杠的!其實上面說了這么多,具體的情景使用還是要看項目的具體情況,在性能面前有些還是要取舍的,但千萬不能為了優化而優化。優化也是不斷積累的過程,不要指望立竿見影。願大家都能寫出一手漂亮的布局。最后覺得不錯的點個贊哈!