实战:在不自定义控件的情况下,怎么获取这个控件的自定义属性?
先抛出问题,跟我一起复习下LayoutInflator的工作原理吧。LayoutInflator的工作原理(极简)
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
........
try {
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
}
final String name = parser.getName();
......
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
....省略处理root、 attachToRoot的逻辑...
return result;
}
}
看到 createViewFromTag(root, name, inflaterContext, attrs); 根据pull解析xml获取到的节点name,再反射创建View。
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
......
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
......
}
发现它解析xml文件,基于xml文件定义的Layout属性反射创建View。那怎么去干预这个解析xml操作,搞点骚操作。系统源码里给出了提示:
自定义LayoutInflator
public class MyLayoutInflater extends LayoutInflater {
protected MyLayoutInflater(LayoutInflater original, Context newContext) {
super(original, newContext);
setFactory2(new MyFactory());
}
@Override
public LayoutInflater cloneInContext(Context newContext) {
return new MyLayoutInflater(this, newContext);
}
private class MyFactory implements Factory2 {
private final String[] sClassPrefix = {
"android.widget.",
"android.view."
};
int[] attrIds = {
R.attr.abcMyName,
};
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = null;
//自定义控件类型 比如:com.docwei.example.MyView
if (name.contains(".")) {
view = createViewByType(name, null, attrs);
} else {
//系统的控件类型 TextView ImageView等
for (String prefix : sClassPrefix) {
view = createViewByType(name, prefix, attrs);
}
}
if (view != null) {
TypedArray a = context.obtainStyledAttributes(attrs, attrIds);
if (a != null && a.length() > 0) {
//获取自定义属性的值
String abcMyName = a.getString(0);
Log.i("onCreateView", "onCreateView: --->" + abcMyName);
a.recycle();
}
}
return view;
}
private View createViewByType(String name, String prefix, AttributeSet attrs) {
try {
return MyLayoutInflater.this.createView(name, prefix, attrs);
} catch (ClassNotFoundException e) {
return null;
}
}
}
}
针对这个类说几点:
-
- cloneInContext(xx)必须重写的,用于创建新的LayoutInflator。
-
- "android.widget.", "android.view."有什么用? 在xml里的系统控件比如TextView等不是全路径,反射创建对象需要全路径,那这两个路径前缀是系统控件才有的,这里会在创建View时给它拼上,系统源码里面也有这个拼接的操作.
-
- 为啥这里是Factory2,不是上面提到的Factory?源码里面Factory2也是接口,继承Factory,但是自定义LayoutInflator目前只能setFactory2,在测试是发现使用Factory会直接奔溃,提示ClassNotFound。
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyLayoutInflater myLayoutInflater = new MyLayoutInflater(LayoutInflater.from(this), this);
View view = myLayoutInflater.inflate(R.layout.activity_main, null);
setContentView(view);
//观察下日志输出
}
}
attrs.xml 里面新增的属性要定义一下
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="abcMyName" format="string" />
</resources>
build运行结果:
最后输出了这个自定义属性的值,你可能觉得这有什么用,很有用的,可以将自定义的LayoutInflator类改造下,将含有这个属性的View保存起来,在页面显示时,就可以操作含有这个属性的所有View。
比如: a.自定义Factory一个十分有用的使用场景就是实现应用换肤。 b.小红书的平行控件。
AsyncLayoutInflater优化你的布局加载
系统针对LayoutInflator布局加载优化推出了一个支持异步加载的LayoutInflator,可以值得一看源码。用了两样好东西---生产消费模型的阻塞顺序队列和支持对象复用的SynchronizedPool 。