在编写Android布局时总会遇到这样或者那样的痛点,好比:html
invisible
或gone
,仍是多多少少会占用内存的。要解决这些痛点,咱们能够请Android布局优化三剑客出码,它们分别是include
、merge
和ViewStub
三个标签,如今咱们就来认识认识它们吧。在此以前,咱们先来看看咱们本次项目的界面效果:java
界面不复杂,咱们来逐个实现吧。android
include
的中文意思是“包含”、“包括”,当你在一个主页面里使用include
标签时,就表示当前的主布局包含标签中的布局,这样一来,就能很好地起到复用布局的效果了。在那些经常使用的布局好比标题栏和分割线等上面用上它能够极大地减小代码量的。它有两个主要的属性:git
layout
:必填属性,为你须要插入当前主布局的布局名称,经过R.layout.xx的方式引用;id
:当你想给经过include添加进来的布局设置一个id的时候就可使用这个属性,它能够重写插入主布局的布局id。下面咱们就来实战一番。github
咱们先建立一个ViewOptimizationActivity
,而后再建立一个layout_include.xml布局文件,它的内容很是简单,就一个TextView:segmentfault
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:gravity="center_vertical" android:textSize="14sp" android:background="@android:color/holo_red_light" android:layout_height="40dp"> </TextView>
如今咱们就用include
标签,将其添加到ViewOptimizationActivity
的布局中:app
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".ViewOptimizationActivity"> <!--include标签的使用--> <TextView android:textSize="18sp" android:text="一、include标签的使用" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <include android:id="@+id/tv_include1" layout="@layout/layout_include"/> </LinearLayout>
没错,include
的使用就是这么简单,只需指明要包含的布局id就行。除此以外,咱们还给这个include
标签设置了一个id,为了验证它就是layout_include.xml的根布局TextView的id,咱们在ViewOptimizationActivity
中初始化TextView,并给它设置文字:ide
TextView tvInclude1 = findViewById(R.id.tv_include1); tvInclude1.setText("1.1 常规下的include布局");
运行以后能够能够看到以下布局:布局
说明咱们设置的layout和id都是成功的。不过你可能会对id这个属性有疑问:id我能够直接在TextView中设置啊,为何重写它呢?别忘了咱们的目的是复用,当你在一个主布局中使用include
标签添加两个以上的相同布局时,id相同就会冲突了,因此重写它可让咱们更好地调用它和它里面的控件。还有一种状况,假如你的主布局是RelateLayout
,这时为了设置相对位置,你也须要给它们设置不一样的id。性能
除了id以外,咱们还能够重写宽高、边距和可见性(visibility
)这些布局属性。可是必定要注意,单单重写android:layout_height
或者android:layout_width
是不行,必须两个同时重写才起做用。包括边距也是这样,若是咱们想给一个include进来的布局添加右边距的话的完整写法是这样的:
<include android:layout_width="match_parent" android:layout_height="40dp" android:layout_marginEnd="40dp" android:id="@+id/tv_include2" layout="@layout/layout_include"/>
初始化后设置一段文字就能够看到以下的效果了:
能够看到,1.2显然比1.1多了一个右边距。
在1.1中咱们知道了id属性能够重写include
布局的根布局id,但对于根布局里面的布局和控件是无能为力的,若是这时一个布局在主布局中include了屡次,那怎么区别里面的控件呢?
咱们先建立一个layout_include2.xml的布局,它的根布局是FrameLayout
,里面有一个TextView
,它的id是tv_same:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:background="@android:color/holo_orange_light" android:layout_height="wrap_content"> <TextView android:gravity="center_vertical" android:id="@+id/tv_same" android:layout_width="match_parent" android:layout_height="50dp" /> </FrameLayout>
在主布局中添加进去:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".ViewOptimizationActivity"> <!--include标签的使用--> …… <include layout="@layout/layout_include2"/> <include android:id="@+id/view_same" layout="@layout/layout_include2"/> </LinearLayout>
为了区分,这里给第二个layout_include2设置了id。也许你已经反应过来了,没错,咱们就是要建立根布局的对象,而后再去初始化里面的控件:
TextView tvSame = findViewById(R.id.tv_same); tvSame.setText("1.3 这里的TextView的ID是tv_same"); FrameLayout viewSame = findViewById(R.id.view_same); TextView tvSame2 = viewSame.findViewById(R.id.tv_same); tvSame2.setText("1.3 这里的TextView的ID也是tv_same");
可见虽然控件的id虽然相同,可是使用起来是没有冲突的。
include
标签虽然解决了布局重用的问题,却也带来了另一个问题:布局嵌套。由于把须要重用的布局放到一个子布局以后就必须加一个根布局,若是你的主布局的根布局和你须要include的根布局都是同样的(好比都是LinearLayout
),那么就至关于在中间多加了一层多余的布局了。那么有没有办法能够在使用include
时不增长布局层级呢?答案固然是有的,那就是使用merge
标签。
使用merge
标签要注意一点:必须是一个布局文件中的根节点,看起来跟其余布局没什么区别,但它的特别之处在于页面加载时它的不会绘制的。打个比方,它就像是布局或者控件的搬运工,把“货物”搬到主布局以后就会功成身退,不会占用任何空间,所以也就不会增长布局层级了。这正如它的名字同样,只起“合并”做用。
咱们来验证一下,首先建立一个layout_merge.xml,在根节点使用merge
标签:
<?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="wrap_content"> <TextView android:id="@+id/tv_merge1" android:text="我是merge中的TextView1" android:background="@android:color/holo_green_light" android:gravity="center" android:layout_width="wrap_content" android:layout_height="40dp" /> <TextView android:layout_toEndOf="@+id/tv_merge1" android:id="@+id/tv_merge2" android:text="我是merge中的TextView2" android:background="@android:color/holo_blue_light" android:gravity="center" android:layout_width="match_parent" android:layout_height="40dp" /> </merge>
这里我使用了一些相对布局的属性,缘由后面你就知道了。咱们接着在ViewOptimizationActivity
的布局添加RelativeLayout
,而后使用include
标签将layout_merge.xml添加进去:
<RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <include android:id="@+id/view_merge" layout="@layout/layout_merge"/> </RelativeLayout>
运行出来的效果图:
在layout_merge.xml中,咱们使用相对布局的属性android:layout_toEndOf
将蓝色TextView设置到了绿色TextView的右边,而layout_merge.xml的父布局是RelativeLayout
,因此这个属性是起了做用了,merge
标签不会影响里面的控件,也不会增长布局层级。
若是你还不放心,能够用Android Studio来检查。我用的Android Studio是3.1版本的,能够经过Layout Inspector查看布局层级,不过记得要先在真机或者模拟器上把项目跑起来。依次点击Tools-Layout Inspector,而后选择你要查看的Activity,就能够看到以下的层级图:
能够看到RelativeLayout
下面直接就是两个TextView了, merge
标签并无增长布局层级。从这里也能够看出merge
的局限性,即你须要明确将merge
里面的布局和控件include
到什么类型的布局中,才能提早设置好merge
里面的布局和控件的位置。
在学习include
标签时咱们知道,它的android:id
属性能够重写被include的根布局id,但若是根节点是merge
呢?前面说了merge
并不会做为一个布局绘制出来,因此这里给它设置id是不起做用的。咱们能够在它的父布局RelativeLayout
中再加一个TextView,使用android:layout_below
属性把设置到layout_merge下面:
<RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <include android:id="@+id/view_merge" layout="@layout/layout_merge"/> <TextView android:text="我不是merge中的布局" android:layout_below="@+id/view_merge" android:background="@android:color/holo_purple" android:gravity="center" android:layout_width="match_parent" android:layout_height="40dp"/> </RelativeLayout>
运行以后你会发现新加的TextView会把merge布局盖住,没有像预期那样在其下方。若是把android:layout_below
中的id改成layout_merge.xml中任一TextView的id(好比tv_merge1),运行以后就能够看到以下效果:
这也符合2.2中的状况,即父布局RelativeLayout
下级布局就是include进去的TextView了。
你必定遇到这样的状况:页面中有些布局在初始化时不必显示,可是又不得不事先在布局文件中写好,虽然设置成了invisible
或gone
,可是在初始化时仍是会加载,这无疑会影响页面加载速度。针对这一状况,Android为咱们提供了一个利器————ViewStub
。这是一个不可见的,大小为0的视图,具备懒加载的功能,它存在于视图层级中,但只会在setVisibility()
和inflate()
方法调用只会才会填充视图,因此不会影响初始化加载速度。它有如下三个重要属性:
android:layout
:ViewStub须要填充的视图名称,为“R.layout.xx”的形式;android:inflateId
:重写被填充的视图的父布局id。与include
标签不一样,ViewStub
的android:id
属性是设置ViewStub
自己id的,而不是重写布局id,这一点可不要搞错了。另外,ViewStub
还提供了OnInflateListener
接口,用于监听布局是否已经加载了。
咱们先建立一个layout_view_stub.xml,里面放置一个Switch
开关:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:background="@android:color/holo_blue_dark" android:layout_height="100dp"> <Switch android:id="@+id/sw" android:layout_gravity="center" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </FrameLayout>
而后在Activity的布局中修改以下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".ViewOptimizationActivity"> <!--ViewStub标签的使用--> <TextView android:textSize="18sp" android:text="三、ViewStub标签的使用" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ViewStub android:id="@+id/view_stub" android:inflatedId="@+id/view_inflate" android:layout="@layout/layout_view_stub" android:layout_width="match_parent" android:layout_height="100dp" /> <LinearLayout android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content"> <Button android:text="显示" android:id="@+id/btn_show" android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content" /> <Button android:text="隐藏" android:id="@+id/btn_hide" android:layout_weight="1" android:layout_width="0dp" android:layout_height="wrap_content" /> <Button android:text="操做父布局控件" android:id="@+id/btn_control" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout>
在ViewOptimizationActivity
中监听ViewStub
的填充事件:
viewStub.setOnInflateListener(new ViewStub.OnInflateListener() { @Override public void onInflate(ViewStub viewStub, View view) { Toast.makeText(ViewOptimizationActivity.this, "ViewStub加载了", Toast.LENGTH_SHORT).show(); } });
而后经过按钮事件来填充和显示layout_view_stub:
@Override public void onClick(View view) { switch (view.getId()) { case R.id.btn_show: viewStub.inflate(); break; case R.id.btn_hide: viewStub.setVisibility(View.GONE); break; default: break; } }
运行以后,点击“显示”按钮,layout_view_stub显示了,并弹出"ViewStub加载了"的Toast;点击“隐藏”按钮,布局又隐藏掉了,可是再点击一下“显示”按钮,页面竟然却闪退了,查看日志,发现抛出了一个异常:
java.lang.IllegalStateException: ViewStub must have a non-null ViewGroup viewParent
咱们打开ViewStub
的源码,看看是哪里抛出这个异常的。很快咱们就能够定位到是在inflate()
方法中
public View inflate() { final ViewParent viewParent = getParent(); if (viewParent != null && viewParent instanceof ViewGroup) { if (mLayoutResource != 0) { final ViewGroup parent = (ViewGroup) viewParent; final View view = inflateViewNoAdd(parent); replaceSelfWithView(view, parent); mInflatedViewRef = new WeakReference<>(view); if (mInflateListener != null) { mInflateListener.onInflate(this, view); } return view; } else { throw new IllegalArgumentException("ViewStub must have a valid layoutResource"); } } else { throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent"); } }
注意到if
语句中有一个replaceSelfWithView()
方法,听这名字就让人有一种不祥的预感了,点进去一看:
private void replaceSelfWithView(View view, ViewGroup parent) { final int index = parent.indexOfChild(this); parent.removeViewInLayout(this); final ViewGroup.LayoutParams layoutParams = getLayoutParams(); if (layoutParams != null) { parent.addView(view, index, layoutParams); } else { parent.addView(view, index); } }
果真,ViewStub
在这里调用了removeViewInLayout()
方法把本身从布局移除了。到这里咱们就明白了,ViewStub
在填充布局成功以后就会自我销毁,再次调用inflate()
方法就会抛出IllegalStateException异常了。此时若是想要再次显示布局,能够调用setVisibility()
方法。
为了不inflate()
方法屡次调用,咱们能够采用以下三种方式:
咱们能够捕获异常,同时调用setVisibility()
方法显示布局。
try { viewStub.inflate(); } catch (IllegalStateException e) { Log.e("Tag",e.toString()); view.setVisibility(View.VISIBLE); }
声明一个布尔值变量isViewStubShow
,默认值为false,布局填充成功以后,在监听事件onInflate
方法中将其置为true。
if (isViewStubShow){ viewStub.setVisibility(View.VISIBLE); }else { viewStub.inflate(); }
我先来看看ViewStub
中的setVisibility()
源码:
public void setVisibility(int visibility) { if (mInflatedViewRef != null) { View view = mInflatedViewRef.get(); if (view != null) { view.setVisibility(visibility); } else { throw new IllegalStateException("setVisibility called on un-referenced view"); } } else { super.setVisibility(visibility); if (visibility == VISIBLE || visibility == INVISIBLE) { inflate(); } } }
能够看到,在inflate()
初始化mInflatedViewRef
以前,若是设置visibility
为VISIBLE
的话是会调用inflate()
方法的,在mInflatedViewRef
不为null以后就不会再去调用inflate()
了。
在显示ViewStub中的布局时,你可能会采起以下的写法:
if (viewStub.getVisibility() == View.GONE){ viewStub.setVisibility(View.VISIBLE); }else { viewStub.setVisibility(View.GONE); }
恭喜你,踩到一个大坑了。这样写你会发现点击“显示”按钮后ViewStub
里面的布局不会再显示出来,也就是说if语句里面的代码没有执行。若是你将viewStub.getVisibility()
的值打印出来,就会看到它始终为0,这偏偏是View.VISIBLE
的值。奇怪,咱们明明写了viewStub.setVisibility(View.GONE)
,layout_view_stub也隐藏了,为何ViewStub
的状态仍是可见呢?
从新回到3.1.3,看看ViewStub
中的setVisibility()
源码,首先判断弱引用对象mInflatedViewRef
是否为空,不为空则取出存放进去的对象,也就是咱们ViewStub
中的View,而后调用了view的setVisibility()
方法,mInflatedViewRef
为空时,则判断visibility
为VISIBLE
或INVISIBLE
时调用inflate()
方法填充布局,若是为GONE
的话则不予处理。这样一来,在mInflatedViewRef
不为空,也就是已经填充了布局的状况下,ViewStub
中的setVisibility()
方法其实是在设置内部视图的可见性,而不是ViewStub
自己。这样的设计其实也符合ViewStub
的特性,即填充布局以后就自我销毁了,给其设置可见性是没有意义的。
仔细比较一下,其实ViewStub
就像是一个懒惰的include
,咱们须要它加载时才加载。要操做布局里面的控件也跟include
同样,你能够先初始化ViewStub
中的布局中再初始化控件:
//一、初始化被inflate的布局后再初始化其中的控件, FrameLayout frameLayout = findViewById(R.id.view_inflate);//android:inflatedId设置的id Switch sw = frameLayout.findViewById(R.id.sw); sw.toggle();
若是主布局中控件的id没有冲突,能够直接初始化控件使用:
//二、直接初始化控件 Switch sw = findViewById(R.id.sw); sw.toggle();
好了,关于ViewStub
的知识就讲这么多了。
本来觉得知识点不难,应该能够写得快一点的,没想到仍是断断续续写了四五天,写得本身都以为有点累了。但愿仍是能对你们有点帮助,不足之处还望指正。下面使用思惟导图总计一下,并给出GitHub上的源码吧。