1 问题引入:
在开发应用程序的时候,经常会遇到这样的情况,会在运行时根据条件来决定哪个View或某个布局的显示与隐藏。你会怎么做呢?
2 解决方案:
2.1 方案1:View.setVisibility
最常见的想法就是先把View都写到布局中,把它们的可见性设为View.Gone,然后在代码更改它的可见性。
这种做法的优点就是逻辑简单而且控制起来比较灵活,但是缺点也很明显就是耗费资源(尤其布局复杂且多个这种View而且要求性能高的APP)。
虽然把View的初始化可见设置为View.Gone,但是在inflate布局的时候View任然会被inflate,也就是说还是会创建对象,会被实例化,会被设置属性,这些在一定程度上肯定会耗费内存等资源的。
2.2 方案2:ViewStub
ViewStub,正如它的名字一样,是一个占位的View,本质上来说它就是一个宽高都为0的一个View,且默认是不可见的,只有在需要的时候,调用setVisibility方法或者inflate方法才会将其要装载的目标布局给加载出来,从而达到延迟加载的效果。这样就能优化页面渲染的速度。
2.3 View.setVisibility与ViewStub对比
下表列出了View.setVisibility与ViewStub在各个阶段的对比,可以看出性能方面: ViewStub>View.Gone>View.INVISIBLE>View.View.Visible
3 ViewStub的用法
布局中使用ViewStub占位
<ViewStub
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:id="@+id/view_stub_tv"
android:inflatedId="@+id/tv_inflate"
android:layout="@layout/view_stub_content_text_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:id="@+id/view_stub_tv": ViewStub的id android:inflatedId="@+id/tv_inflate" :被装载的View的id android:layout="@layout/view_stub_content_text_layout":被装载的View的布局
view_stub_content_text_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_view_stub_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="这是 view stub 真正的内容">
</TextView>
注意:即使被装载的View布局中有id也是无效的,还是要用使用android:inflated中声明的id。
4 装载:
ViewStub本质是一个空View,因此可以像其他View一样通过findViewById拿到。
val tvStub= findViewById<ViewStub>(R.id.view_stub_tv)
此时它还是一个ViewStub,并不是真正装载的View。我们使用ViewStub的目的就是在需要的时机才去加载View,以达到延迟加载的目的。
ViewStub加载真正的View有两种方式:
4.1 ViewStub.inflate方法:
tvView = findViewById<ViewStub>(R.id.view_stub_tv).inflate() as TextView
注意,inflate后产生的对象类型是View类型,如果要得到真正的类型,需要转换为真正的类型。
4.2 ViewStub.setVisibility方法:
/**
* When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE},
* {@link #inflate()} is invoked and this StubbedView is replaced in its parent
* by the inflated layout resource. After that calls to this function are passed
* through to the inflated view.
*
* @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
*
* @see #inflate()
*/
@Override
@android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
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();
}
}
}
可以看出,mInflatedViewRef就是被装载的View。
刚开始时还没有装载,mInflatedViewRef为空,于是走到else分支,调用inflate方法进行加载。
已经装载过后,mInflatedViewRef不为空,走if分支,调用View.setVisibility。
因此,ViewStub.setVisibility本质上是对ViewStub.inflate和View.setVisibility的封装,方便对ViewStub显示和隐藏用的。
5 Demo使用
activity_view_stub_test
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<ViewStub
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:id="@+id/view_stub_tv"
android:inflatedId="@+id/tv_inflate"
android:layout="@layout/view_stub_content_text_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="显示"
android:onClick="show"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="隐藏"
android:onClick="hide"
app:layout_constraintBottom_toTopOf="@+id/button4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
view_stub_content_text_layout
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tv_view_stub_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="这是 view stub 真正的内容">
</TextView>
主页面:
package imageviewtype
import android.os.Bundle
import android.util.Log
import android.view.View
import android.view.ViewStub
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.timelinedemo.R
class ViewStubTestActivity : AppCompatActivity() {
private var tvView: TextView? = null
private var pvStub: ViewStub? = null
private var button5: Button? = null //隐藏
private var button4: Button? = null //显示
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_stub_test)
button5 = findViewById(R.id.button5)
button4 = findViewById(R.id.button4)
button4?.setOnClickListener {
show(it)
}
button5?.setOnClickListener {
hide(it)
}
}
fun show(view: View) {
// ViewStub 只能 inflate 一次,因此这里判断只有在没装载时,才进行 inflate
if (tvView == null) {
// viewStub 的 ID: view_stub_tv
Log.d("tanyonglin","View没有被装载过")
tvView = findViewById<ViewStub>(R.id.view_stub_tv).inflate() as TextView
// 得到被装载的 TextView,可以修改其文本
tvView?.text = "你好啊"
} else {
Log.d("tanyonglin","View被装载了已经")
tvView?.visibility = View.VISIBLE
}
}
fun hide(view: View) {
tvView?.visibility = View.GONE
}
}
总结:ViewStub只能被inflate一次,因为inflate后,ViewStub就不存在了,被替换成真正的View。
下篇分析下ViewStub源码。