Android UI布局优化之ViewStub介绍

2019-09-19 14:10:03

参考地址  Android UI布局优化之ViewStub

前言:

在设计模式的单利模式中,懒汉式和饿汉式是其中两种。

一种是在类被加载的时候就完成单例对象的初始化,一种是在需要使用该单例的时候才初始化。

在android的视图设计中,同样需要使用的这样的设计模式。

这样的视图加载起来需要耗费很多的时间。在这几百个视图里面,可能有部分视图是在点击某一按钮也就是并不是马上加载,

而是延迟到要使用的时候才加载这部分视图。也就是类似于单例模式中的懒加载。

1

2

3

4

5

特性:

1.  ViewStub是一个继承了View类的视图。

2.  ViewStub是不可见的,实际上是把宽高都设置为0

3.  可以通过布局文件的android:inflatedId或者调用ViewStub的setInflatedId方法为懒加载视图的跟节点设置ID

4.  ViewStub视图在首次调用setVisibility或者inflate方法之前,一直存在于视图树中

5.  只需要调用ViewStub的setVisibility或者inflate方法即可显示懒加载的视图

6.  调用setVisibility或者inflate方法之后,懒加载的视图会把ViewStub从父节点中替换掉

7.  ViewStub的inflate只能被调用一次,第二次调用会抛出异常,setVisibility可以被调用多次,但不建议这么做(后面说原因)

8.  为ViewStub赋值的android:layout_属性会替换待加载布局文件的根节点对应的属性

9.  inflate方法会返回待加载视图的根节点

1

2

3

4

5

6

7

8

9

使用:

我在一个activity上放置了一个按钮,点击后加载懒加载的视图。


Activity布局文件定义my_sub_activity.xml:


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


    <Button

        android:onClick="onClick"

        android:layout_width="match_parent"

        android:layout_height="wrap_content"

        android:text="加载视图"/>


    <ViewStub

        android:id="@+id/stub"

        android:inflatedId="@+id/subTree"

        android:layout="@layout/my_sub_tree"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />


</LinearLayout>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

其中android:inflatedId指定了懒加载视图跟节点的ID。android:layout指定了懒加载的视图。android:layout_width和android:layout_height分别指定了懒加载视图的宽和高。


懒加载布局文件my_sub_tree.xml:


<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:background="#ffffff"

    android:gravity="center"

    android:padding="10dip"

    android:text="懒加载视图"

    android:textColor="#000000"

    android:textSize="22sp">

</TextView>

1

2

3

4

5

6

7

8

9

10

11

懒加载视图里只有一个TextView(这里只是做测试,正常情况下这里应该是一个复杂的视图)。


ViewStubActivity的代码:


public class ViewStubActivity extends Activity implements View.OnClickListener {


    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.my_sub_activity);

    }


    @Override

    public void onClick(View v) {

        // 这里调用的是inflate方法,当然,也可以调用setVisibility方法(但是不建议这么做)

        // 只能点击一次加载视图按钮,因为inflate只能被调用一次

        // 如果再次点击按钮,会抛出异常"ViewStub must have a non-null ViewGroup viewParent"

        ((ViewStub) findViewById(R.id.stub)).inflate();

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

代码里设置了布局,并在点击后查到到ViewStub对象,并加载视图。


下面看看加载视图前后的对比图:

 


为了说明视图树在加载前后的对比,我使用hierarchyviewer视图树查看工具,做了一个前后对比图:

加载前视图树:



加载后视图树:



从上面的两个视图树中我们明显发现,ViewStub节点被TextView替换。

也就是说,在调用inflate方法之前,ViewStub一直存在于视图树中,当调用inflate之后,ViewStub被加载的视图替换,到此,ViewStub的作用完成,之后ViewStub可能被内存回收(如果没有声明成成员变量的话,也就是没有强引用)


源码解析:

下面针对ViewStub的特性对源码进行解析:

特性一:ViewStub是一个继承了View类的视图。


public final class ViewStub extends View {


特性二:ViewStub是不可见的,实际上是把宽高都设置为0


@Override

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    // 测量的时候,告诉父节点自己需要的空间为0

    setMeasuredDimension(0, 0);

}


@Override

public void draw(Canvas canvas) {

    // 不绘制

}


@Override

protected void dispatchDraw(Canvas canvas) {

    // 不分发绘制事件

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

ViewStub在计算的时候,为自己请求的宽高都为0,并重写了绘制相关的方法,但不做任何事情。


特性三:可以通过布局文件的android:inflatedId或者调用ViewStub的setInflatedId方法为懒加载视图的跟节点设置ID(如果跟视图未设置ID的话)


public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {

    TypedArray a = context.obtainStyledAttributes(

            attrs, com.android.internal.R.styleable.ViewStub, defStyleAttr, defStyleRes);

    // 通过自属性inflatedId来获取加载的视图跟节点ID,默认返回NO_ID,也就是-1,代表没有赋值id

    mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);

    // 需要加载的视图资源ID

    mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);


    a.recycle();


    a = context.obtainStyledAttributes(

            attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);

    // 为自己赋值ID,没有则赋值为-1

    mID = a.getResourceId(R.styleable.View_id, NO_ID);

    a.recycle();


    // 初始化视图

    initialize(context);

}


private void initialize(Context context) {

    mContext = context;

    // 设置视图不可见

    setVisibility(GONE);

    // 设置当前视图不可绘制

    setWillNotDraw(true);

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

初始化的时候,从配置文件中取出了inflatedId和待加载的资源文件id以及自身的id,

最后,调用了initialize将自身设置为不可见,并设置为不可重绘,最大限度减少资源占用。


最后看看什么时候ViewStub执行加载视图操作:

首先是inflate方法:


/**

 * Inflates the layout resource identified by {@link #getLayoutResource()}

 * and replaces this StubbedView in its parent by the inflated layout resource.

 *

 * @return The inflated layout resource.

 *

 */

public View inflate() {

    // 拿到父节点

    final ViewParent viewParent = getParent();


    if (viewParent != null && viewParent instanceof ViewGroup) {

        // 判断父节点不为空,并且是容器,则进行视图加载

        if (mLayoutResource != 0) {

            // 必须在布局文件中,或者是调用setLayoutResource方法设置待加载的视图资源文件ID

            final ViewGroup parent = (ViewGroup) viewParent;

            final LayoutInflater factory;

            // mInflater是外部设置进来的,通过setLayoutInflater方法设置

            if (mInflater != null) {

                factory = mInflater;

            } else {

                // 如果外部未设置视图加载器,初始化

                factory = LayoutInflater.from(mContext);

            }

            // 加载视图,得到视图根节点

            final View view = factory.inflate(mLayoutResource, parent,

                    false);


            if (mInflatedId != NO_ID) {

                // 如果有设置inflateId,则赋值给根节点(如果根节点自己有id,会被覆盖)

                view.setId(mInflatedId);

            }


            // 得到ViewStub在父节点中的位置

            final int index = parent.indexOfChild(this);

            // 从父节点中移除ViewStub(到此,ViewStub从视图树种移除)

            parent.removeViewInLayout(this);


            // 得到ViewStub在布局文件中定义的android:layout_*的属性

            final ViewGroup.LayoutParams layoutParams = getLayoutParams();

            // 将懒加载视图添加到ViewStub的父节点(到此,ViewStub被完全替换)

            if (layoutParams != null) {

                parent.addView(view, index, layoutParams);

            } else {

                parent.addView(view, index);

            }


            // 将懒加载的视图使用弱引用进行引用(给setVisibility方法使用,后面会讲)

            mInflatedViewRef = new WeakReference<View>(view);


            // 视图加载成功后调用回调方法。

            if (mInflateListener != null) {

                mInflateListener.onInflate(this, view);

            }


            // 返回加载后布局文件的根节点

            return view;

        } else {

            // 如果未设置layoutResource也就是待加载的视图,则抛出异常

            throw new IllegalArgumentException("ViewStub must have a valid layoutResource");

        }

    } else {

        // 如果ViewStub的父节点为空,因为ViewStub成功执行inflate方法后

        // 会调用parent.removeViewInLayout(this);将自己从父节点移除

        // 所以ViewStub的inflate只能调用一次

        throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

ViewStub的inflate方法简要的讲就是把自己从父亲从移除,把待加载的视图加入到父节点中,

并把自己所有的layout属性给待加载的视图,

什么是layout属性呢,也就是下面以”android:layout_”打头的属性:

如android:layout_width以及layout_height,

所以这里大家需要小心自己的待加载视图的根节点的android:layout_属性被替换掉。


<ViewStub

    android:id="@+id/stub"

    android:inflatedId="@+id/subTree"

    android:layout="@layout/my_sub_tree"

    android:layout_width="wrap_content"

    android:layout_height="wrap_content" />

1

2

3

4

5

6

接着我们有提到,调用ViewStub的setVisibility也可以加载待加载视图:


public void setVisibility(int visibility) {

    if (mInflatedViewRef != null) {

        // 如果对待加载视图的软引用不为空,说明已经执行过inflate方法了

        // 因为在inflate方法执行成功后有对其赋值

        View view = mInflatedViewRef.get();

        if (view != null) {

            // 如果引用的视图未被垃圾回收器回收,则设置其可见性

            view.setVisibility(visibility);

        } else {

            // 如果引用的视图已经被垃圾回收器回收,则抛出异常

            // 这也就是为什么setVisibility可以调用多次,但是并不推荐这样做的原因

            throw new IllegalStateException("setVisibility called on un-referenced view");

        }

    } else {

        // 如果弱引用对象未初始化,则说明未调用inflate

        // 设置自身可见性

        super.setVisibility(visibility);

        if (visibility == VISIBLE || visibility == INVISIBLE) {

                // 如果传入的是VISIBLE或者INVIBLE,则调用inflate加载视图

            inflate();

        }

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

另外ViewStub还提供了一系列方法,供用户设置属性:


/** 获取待加载视图的根节点ID */

public int getInflatedId() {

    return mInflatedId;

}


/** 设置待加载视图的根节点ID */

public void setInflatedId(int inflatedId) {

    mInflatedId = inflatedId;

}


/** 获取待加载视图的资源文件ID */

public int getLayoutResource() {

    return mLayoutResource;

}


/** 设置待加载视图的资源文件ID */

public void setLayoutResource(int layoutResource) {

    mLayoutResource = layoutResource;

}


/** 设置布局加载器 */

public void setLayoutInflater(LayoutInflater inflater) {

    mInflater = inflater;

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

总结

ViewStub标签需要必须通过android:layout属性指定待加载的视图资源文件ID,否则会抛异常(在inflate方法被调用前,通过setLayoutResource也可以设置待加载的视图资源文件ID,但不建议这样做)。

ViewStub标签的所有android:layout_打头的属性,都会替换待加载视图的跟布局对应属性

最好通过ViewStub的inflate方法加载视图,该方法会返回视图根节点。

inflate方法只能调用一次,不建议通过setVisibility加载视图

如果需要通过findViewById查找待加载视图中的节点,需要在inflate方法执行之后,否则会找不到

关于UI布局的优化,还有include和merge两种方式,大家可以参阅:Android 布局优化之include与merge



  • 2019-08-06 15:30:08

    小程序展示富文本

    然而rich-text有个问题,它不能够给内联img设置宽度100%,这样图片就容易溢出屏幕。我们只能在后台返回富文本的时候对图片的img标签进行格式化。或者前端进行格式化也行,我赞成使用后端,因为很多种情况我们不一定都能想得到。

  • 2019-08-07 09:07:32

    最全的Service Worker讲解

    Service Worker 最主要的特点是:在页面中注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的 HTTP 请求。 基于 Service Worker API 的特性,结合 Fetch API、Cache API、Push API、postMessage API 和 Notification API,可以在基于浏览器的 web 应用中实现如离线缓存、消息推送、静默更新等 native 应用常见的功能,以给 web 应用提供更好更丰富的使用体验。

  • 2019-08-07 09:09:19

    windows系统下定时关闭程序

    其中xxx.exe是你要关闭的进程中运行的exe,可以ctrl+alt+del打开任务管理器,进到详细信息查看 然后把.txt文件后缀改成.bat(此时要在查看一栏勾上文件拓展名,要不还是txt文档)

  • 2019-08-07 09:16:43

    一个比较完美的PWA例子

    但就目前来讲,PWA是Google主推的一项技术标准,FireFox,Chrome以及一些基于Blink的浏览器已经支持渐进式Web应用了,Edge上对渐进式Web应用的支持还在开发。Apple公司也表示会考虑在自己Safari支持PWA。然而这项功能已经进入了WebKit内核的五年计划中。长期来看,对浏览器兼容性的支持方面应该已经不算太大问题了。况且在现阶段,在不支持渐进式Web应用的浏览器中,你的应用也只是无法使用渐进式Web应用的离线功能而已,除此之外的功能均可以正常使用。

  • 2019-08-07 09:57:48

    spring data jpa 实体类中字段不与数据库表映射

    当我们使用spring data jpa开发的时候,会将实体类中的成员变量与表中的字段一一对应,当我们在实体类中加上一个不与数据库表一一对应的成员变量的时候,此时我们只要在这个成员变量上加上注解@Transient @

  • 2019-08-07 17:16:53

    如何在 Node.js 中使用 import / export 的三种方法

    因为一些历史原因,虽然 Node.js 已经实现了 99% 的 ES6 新特性,不过截止 2018.8.10,How To Enable ES6 Imports in Node.JS 仍然是老大难问题,下面我来介绍三种方法可以让我们在 Node.js 中使用 import/export 。

  • 2019-08-13 08:56:46

    nuxtjs组合element

    添加elementUI 插件,plugins->ele.js,代码如下