ViewStub的使用
xml文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/btn_vs_showView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="显示ViewStub"/>
<Button
android:id="@+id/btn_vs_changeHint"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="更改ViewStub"/>
<Button
android:id="@+id/btn_vs_hideView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_weight="1"
android:text="隐藏ViewStub"/>
</LinearLayout>
<!--ViewStub 展示或者隐藏内容-->
<ViewStub
android:id="@+id/viewstub_test"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inflatedId="@+id/iv_VsContent"
android:layout="@layout/iv_vs_content"/>
</RelativeLayout>
可以看到,ViewStub必须添加layout,这个是它需要展示的东西。inflatedId则可以加也可以不加,并不影响显示。它的作用是inflateId 表示给被引用/填充的 layout 资源设置一个id,通过它可以获取到被引用/填充的 layout 的 View 实例。
然后显示的时候可以调用inflate()得到一个view.
View view = viewstub_test.inflate();
隐藏的时候调用setVisibility()
viewstub_test.setVisibility(View.INVISIBLE);
源码分析
ViewStub的创建
public final class ViewStub extends View{}
ViewStub是集成于View的,所以它的创建也是调用createViewFromTag(),createViewFromTag有多个重载方法,最终调用的是
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
......
try {
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
}
}
主要看下面的代码
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
根据标签里面有没有.来区分是自定义的控件,还是系统自带的控件
<TextView
android:id="@+id/tvNum"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="200dp"
android:layout_marginLeft="50dp"
android:text="18 cm"
android:textColor="#ffffffff"
android:textSize="42sp"
android:textStyle="bold"
/>
<com.example.customview.ruler.RulerView
android:id="@+id/ruler_view"
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:layout_height="200dp"/>
从上面可以知道,系统的控件在使用的时候直接写这个控件就好,而自定义的控件则需要写全路径。
@Nullable
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Objects.requireNonNull(viewContext);
Objects.requireNonNull(name);
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
}
try {
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
}
}
在createView()主要做了以下几件事。
1、从map中获取构造函数对象,如果不存在,则通过反射的方式创建一个实例,并保存在map中。
2、创建好构造函数实例之后调用newInstance(),创建一个view.如果view属于ViewStub,则返回去。此时ViewStub则创建好了。
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);
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
a.recycle();
setVisibility(GONE);
setWillNotDraw(true);
}
在ViewStub构造函数中,setVisibility(GONE)表示将视图设置为不可见,并且不会去绘制。这也是在布局优化的时候会使用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();
}
}
}
当我们第一次进去的时候mInflatedViewRef是为null的,则会走else逻辑。
由于我们在构造方法中setVisibility(GONE),则不会走inflate(),这样就不会显示ViewStub所指定的layout资源。那如果想要加载ViewStub所指定的layout资源,需要设置ViewStub控件设置可见,或者调用inflate()方法。
特别注意:
setVisibility(int visibility)方法,参数visibility对应三个值分别是INVISIBLE、VISIBLE、GONE
VISIBLE:视图可见
INVISIBLE:视图不可见的,它仍然占用布局的空间
GONE:视图不可见,它不占用布局的空间
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");
}
}
在inflate()主要做了以下几件事:
1、调用inflateViewNoAdd方法返回android:layout指定的布局文件最顶层的view
2、调用replaceSelfWithView方法, 移除ViewStub, 添加view到被移除的ViewStub的位置
3、初始化mInflatedViewRef,添加view到 mInflatedViewRef 中
4、加载完成之后,回调onInflate 方法
这样第二次进来的时候,在setVisibility()就会走if的逻辑,通过setVISIBLE或者INVISIBLE来显示和不显示。
ViewStub的注意事项
1、使用ViewStub需要在xml中设置android:layout,不是layout,否则会抛出异常
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
2、ViewStub不能作为根布局,它需要放在ViewGroup中, 否则会抛出异常
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
3、一旦调用setVisibility(View.VISIBLE)或者inflate()方法之后,该ViewStub将会从试图中被移除(此时调用findViewById()是找不到该ViewStub对象).
// 获取ViewStub在视图中的位置
final int index = parent.indexOfChild(this);
// 移除ViewStub
// 注意:调用removeViewInLayout方法之后,调用findViewById()是找不到该ViewStub对象
parent.removeViewInLayout(this);
4、如果指定了mInflatedId , 被inflate的layoutView的id就是mInflatedId
// mInflatedId 是在xml设置的 inflateId
if (mInflatedId != NO_ID) {
// 将id复制给view
view.setId(mInflatedId);
//注意:如果指定了mInflatedId , 被inflate的layoutView的id就是mInflatedId
}
5、被inflate的layoutView的layoutParams与ViewStub的layoutParams相同.
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
// 将xml中指定的 android:layout 布局文件中最顶层的View 也就是根view,
// 添加到被移除的 ViewStub的位置
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
网友评论