专栏名称: 安卓开发精选
伯乐在线旗下账号,分享安卓应用相关内容,包括:安卓应用开发、设计和动态等。
目录
相关文章推荐
开发者全社区  ·  字节跳动出手:直接辞退353人! ·  2 天前  
开发者全社区  ·  yc女同学的朋友圈 ·  2 天前  
开发者全社区  ·  但佬朋友圈刷屏 ·  2 天前  
开发者全社区  ·  坐标北京 89年自救相亲帖 ·  3 天前  
51好读  ›  专栏  ›  安卓开发精选

Android 布局优化,看这 3 点就够了!

安卓开发精选  · 公众号  · android  · 2019-10-09 19:40

正文

(给 安卓开发精选 加星标)

转自: Android技术

https://www.jianshu.com/p/2ee61b88175e

前言


在编写Android布局时总会遇到这样或者那样的痛点,比如:


  1. 有些布局的在很多页面都用到了,而且样式都一样,每次用到都要复制粘贴一大段,有没有办法可以复用呢?

  2. 解决了1中的问题之后,发现复用的布局外面总要额外套上一层布局,要知道布局嵌套是会影响性能的呐;

  3. 有些布局只有用到时才会显示,但是必须提前写好,虽然设置了为invisible或gone,还是多多少少会占用内存的。


要解决这些痛点,我们可以请Android布局优化三剑客出码,它们分别是 include、merge和ViewStub 三个标签,现在我们就来认识认识它们吧。 在此之前,我们先来看看我们本次项目的界面效果:



界面不复杂,我们来逐个实现吧。


include


include的中文意思是“包含”、“包括”,当你在一个主页面里使用include标签时,就表示当前的主布局包含标签中的布局,这样一来,就能很好地起到复用布局的效果了。 在那些常用的布局比如标题栏和分割线等上面用上它可以极大地减少代码量的。 它有两个主要的属性:


  1. layout: 必填属性,为你需要插入当前主布局的布局名称,通过 R.layout.xx 的方式引用;

  2. id: 当你想给通过include添加进来的布局设置一个id的时候就可以使用这个属性,它可以重写插入主布局的布局id。


下面我们就来实战一番。


我们先创建一个ViewOptimizationActivity,然后再创建一个layout_include.xml布局文件,它的内容非常简单,就一个TextView:


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的布局中:







    
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">


    
    <TextView
        android:textSize="18sp"
        android:text="1、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,并给它设置文字:


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多了一个右边距。


控件ID相同时的处理


在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 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虽然相同,但是使用起来是没有冲突的。


merge


include 标签虽然解决了布局重用的问题,却也带来了另外一个问题: 布局嵌套。 因为把需要重用的布局放到一个子布局之后就必须加一个根布局,如果你的主布局的根布局和你需要include的根布局都是一样的(比如都是 LinearLayout ),那么就相当于在中间多加了一层多余的布局了。 那么有没有办法可以在使用 include 时不增加布局层级呢? 答案当然是有的,那就是使用 merge 标签。


使用 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添加进去:


    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <include
        android:id="@+id/view_merge"
        layout="@layout/layout_merge"/>

RelativeLayout>


运行出来的效果图:



merge标签对布局层级的影响


在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 里面的布局和控件的位置。


merge的ID


在学习 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了。


ViewStub


你一定遇到这样的情况: 页面中有些布局在初始化时没必要显示,但是又不得不事先在布局文件中写好,虽然设置成了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 开关:


<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>


然后在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">


    
    <TextView
        android:textSize="18sp"
        android:text="3、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;







请到「今天看啥」查看全文