今天分享一些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" />