Android ViewStub

1.ViewStub

ViewStub是一个可用于性能优化的控件,它是一个不可见的、零尺寸的View,可以在运行时进行延迟加载一个布局文件,从而提高显示速率。

viewstub和include比较像,都是在一个布局文件中嵌入另外一个布局文件,然而viewstub可以延迟加载,它只会在手动指定加载的时候才会加载这个布局文件,而include则会立即加载。

手动指定加载:当ViewStub的setVisibility(int)方法或inflate()方法被调用时,它才会加载被指定的布局并在父布局中将自己替换为加载的布局。替换后,控件ViewStub会从布局树中移除。控件ViewStub的布局属性会传递给被加载的布局。

因此,不是必须显示的布局就可以使用ViewStub来代替,这样可以减少界面首次加载时资源消耗,提升最初加载速度。

ViewStub实际中常用情景:比如在无数据或者网络错误的时候,需要单独显示一个布局,那么这个布局就可以用ViewStub。

注意:

①对ViewStub的inflate操作只能进行一次,因为inflate的时候是将其指向的布局文件解析inflate并替换掉当前ViewStub本身(由此体现出了ViewStub"占位符"性质),一旦替换后,此时原来的布局文件中就没有ViewStub控件了,因此,如果多次对ViewStub进行infalte,会出现错误信息:ViewStub must have a non-null ViewGroup viewParent。

②ViewStub指向的布局文件解析inflate并替换掉当前ViewStub本身,并不是完全意义上的替换(与include标签还不太一样),替换时,布局文件的layout params是以ViewStub为准,其他布局属性是以布局文件自身为准。

2.ViewStub使用

①布局文件

在主布局中使用ViewStub标签来引入目标布局

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

android:layout_width="match_parent"

android:layout_height="match_parent"

android:orientation="vertical">

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="@string/hello_world" />

<Button

android:id="@+id/toggle"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:onClick="onClick"

android:text="显示/隐藏" />

<ViewStub

android:id="@+id/vs"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:inflatedId="@+id/inflated_id"

android:layout="@layout/view_stub_layout"/>

</LinearLayout>

android:layout指定被加载替换的布局。
android:inflatedId指定被加载替换的布局的id。

其中被加载替换的布局view_stub_layout.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" >

<TextView

android:id="@+id/tv"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:text="vs中的tv" />

</LinearLayout>

②java文件

布局文件写好了,接下来在程序运行时加载这个布局。

public class MainActivity extends Activity {

private ViewStub stub;

private boolean isShow = true;

private TextView tv;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

stub = (ViewStub) findViewById(R.id.vs);

//stub布局的加载有两种方式,一种是stub.inflate();另一种是//stub.setVisibility(View.VISIBLE);+findViewById

View inflatedView = stub.inflate(); //inflate()只能调用一次

//stub.setVisibility(View.VISIBLE);

//View inflatedView = this.findViewById( R.id.inflated_id);

tv = (TextView) inflatedView.findViewById( R.id.tv); //注意要先实例化stub,然后才可以拿到tv

inflatedView.setBackgroundColor( Color.BLUE);

}

public void onClick(View v){

switch (v.getId()) {

case R.id.toggle:

if (isShow) {

stub.setVisibility(View.GONE);

}else{

stub.setVisibility(View.VISIBLE);

tv.setText("---");

}

isShow = !isShow;

break;

default:

break;

}

}

}

官方推荐加载首选方法是inflate()。调用inflate()方法后布局被加载替换,同时返回布局对象。避免了使用findViewById()方法。

注意:inflate()方法只能调用一次,调用被移除而没有了父布局。第二次调用会抛出异常ViewStub must have a non-null ViewGroup viewParent 。

ViewStub可以设置加载监听回调,成功加载后回调,且只会回调一次。

viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {

/**

* @param stub ViewStub 对象

* @param inflated 被加载填充的布局

*/

@Override

public void onInflate(ViewStub stub, View inflated) {

tv = (TextView) inflated.findViewById(R.id.tv);

tv.setText("ShowTitle");

}

}

3.ViewStub源码

①构造方法

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

super(context);

final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewStub, defStyleAttr, defStyleRes);

saveAttributeDataForStyleable(context, R.styleable.ViewStub, attrs, a, defStyleAttr, defStyleRes);

//获取在xml文件中定义的inflatedId属性

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

//获取到xml文件中定义的layout属性

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

//获取xml文件中定义的id属性

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

a.recycle();

//设置ViewStub直接不显示。所以在xml文件中如何控制它的显示属性,都是不显示的

setVisibility(GONE);

// 设置ViewStub不尽兴绘制

setWillNotDraw(true);

}

②ViewStub的onMeasure和onDraw方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//设置宽和高都为0,也就是控件的大小为0

setMeasuredDimension(0, 0);

}

@Override

public void draw(Canvas canvas) {

//不进行任何绘制

}

@Override

protected void dispatchDraw(Canvas canvas) {

}

③inflate()方法

public View inflate() {

final ViewParent viewParent = getParent(); //获取ViewStub在布局文件中的父布局

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

if (mLayoutResource != 0) { //mLayoutResource就是属性layout指定的真正要加载的布局

final ViewGroup parent = (ViewGroup) viewParent;

final View view = inflateViewNoAdd( parent); //把真正要显示的View布局文件渲染成View对象并且给返回

replaceSelfWithView(view, parent); //将ViewStub从布局文件结构中移除,并且把渲染好的View添加到ViewStub所处的位置

mInflatedViewRef = new WeakReference<>(view);

if (mInflateListener != null) {

mInflateListener.onInflate(this, view); //保存当前View对象的弱引用,方便其他地方使用

}

return view; //返回创建的View对象

} else { //当没有为ViewStub指定layut属性时会走这个case,抛出异常

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

}

} else { //第一次调用ViewStub的inflate方法后会把ViewStub从布局文件结构中移除,也就没有了ViewGroup。当第二次调用ViewStub的inflate方法后会走这个case,抛出异常

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

}

}

可以看到当viewParent为空或viewParent不是ViewGroup时就会报ViewStub must have a non-null ViewGroup viewParent错误。第一次调用inflate方法的时候不会报错,肯定是进了if,if里面有一个方法replaceSelfWithView(view,parent),其中参数view就是在布局文件中给viewstub指定的layout所引用的那个布局;参数parent就是getParent方法得到的,也就是activity的填充布局LinearLayout。

其实inflate方法主要的就是inflateViewNoAdd和replaceSelfWithView这两个方法。其中,inflateViewNoAdd方法负责获取到布局渲染器将真正需要展示的布局文件渲染成View并且给返回。replaceSelfWithView方法负责将ViewStub从布局文件结构中移除,同时把渲染好的View添加到ViewStub之前所处的位置。之后把渲染好的View的弱引用给存储起来,方便在setVisibility()方法中使用。

首先看inflateViewNoAdd方法:

private View inflateViewNoAdd(ViewGroup parent) {

final LayoutInflater factory; //布局填充器

if (mInflater != null) {

factory = mInflater;

} else {

factory = LayoutInflater.from(mContext);

}

// 把真正要显示的布局文件渲染成View对象

final View view = factory.inflate( mLayoutResource, parent, false);

// mInflatedId对应android:inflatedId,如果指定了就为渲染好的View给设置进去

if (mInflatedId != NO_ID) {

view.setId(mInflatedId);

}

return view;

}

然后进去replaceSelfWithView方法看一下:

private void replaceSelfWithView(View view, ViewGroup parent) {

final int index = parent.indexOfChild(this); //获取ViewStub在父布局中所处在的位置

parent.removeViewInLayout(this); //将ViewStub从父布局中移除

final ViewGroup.LayoutParams layoutParams = getLayoutParams(); //获取ViewStub的布局参数

if (layoutParams != null) { //当设置了布局参数(例如 android:width="50dp",height="50dp"),就将渲染好的View连同ViewStub的布局参数添加到ViewStub所处的位置

parent.addView(view, index, layoutParams);

} else { //否则,将渲染好的View添加到ViewStub所处的位置

parent.addView(view, index);

}

}

首先通过parent.removeViewInLayout(this);把this也就是viewstub从父布局linearlayout中移除了。然后通过parent.addView()把view(也就是layout引用的布局)添加到了父布局LinearLayout中。

用layout inspector来查看一下:

inflate前:可以看到viewstub是灰色的

inflate后:可以看到viewstub直接被移除了,把引用布局直接放到view树里了。

所以第二次调用inflate方法时,viewstub的parent已经为空了,就会抛出此异常。

当调用textView = viewStub.findViewById( R.id.tv);获取到的textview是空的;而使用textView = findViewById(R.id.tv);就可以直接拿到控件对象。

④setVisibility()方法

public void setVisibility(int visibility) {

if (mInflatedViewRef != null) { //mInflatedViewRef只有在inflate方法中被初始化了,即当真正的布局文件被加载之后才不为空(第二次就不为空)

View view = mInflatedViewRef.get(); //获取到当前的View

if (view != null) {

view.setVisibility(visibility); //当前view不空则直接显示

} else {

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

}

} else { //没有调用inflate的话,会设置可见性(第一次会为空)

super.setVisibility(visibility);

if (visibility == VISIBLE || visibility == INVISIBLE) { //如果当前设置可见性为 VISIBLE或者INVISIBLE的时候,会调用inflate方法

inflate();

}

}

}

首先使用mInflatedViewRef获取到view,然后设置隐藏与显示;mInflatedViewRef是一个view的弱引用WeakReference<View>。其实在上面的inflate方法中已经为其添加了mInflatedViewRef = new WeakReference<>(view);这个view就是viewstub中的引用布局。

所以,使用viewstub可以实现相同的显示或隐藏效果。

可以发现,setVisibility方法中也调用了inflate方法,因此即使没有调用inflate方法,而是直接点击show按钮,引用布局也可以绘制出来。

注意:ViewStub本身是不可见的,对ViewStub的setVisibility(..)与其他控件不一样,ViewStub的setVisibility成View.VISIBLE或INVISIBLE时,如果是首次使用,都会自动inflate其指向的布局文件,并替换ViewStub本身,再次使用则是相当于对其指向的布局文件设置可见性。

注意:

ViewStub支持使用 <include> 标签的布局。

ViewStub不支持使用 <merge> 标签的布局。

相关推荐
还鮟2 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡3 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi003 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
zhangphil5 小时前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android
你过来啊你5 小时前
Android View的绘制原理详解
android
移动开发者1号8 小时前
使用 Android App Bundle 极致压缩应用体积
android·kotlin
移动开发者1号8 小时前
构建高可用线上性能监控体系:从原理到实战
android·kotlin
ii_best13 小时前
按键精灵支持安卓14、15系统,兼容64位环境开发辅助工具
android
美狐美颜sdk13 小时前
跨平台直播美颜SDK集成实录:Android/iOS如何适配贴纸功能
android·人工智能·ios·架构·音视频·美颜sdk·第三方美颜sdk
恋猫de小郭17 小时前
Meta 宣布加入 Kotlin 基金会,将为 Kotlin 和 Android 生态提供全新支持
android·开发语言·ios·kotlin