近期有一个比较特殊的需求,就是给所有View上添加一个特殊属性,可以在XML中赋值(例如:tag = "https://static.byr.cn/files/imgupload/2011-07-22-00-24-26.jpg"),在View创建时根据属性做一些操作。如载入URL等。
这个需求有些限制:
1.因为希望是通用的架构,各种View如ImageView、各种Layout、还有自定义View等都可以使用。
所以没法去继承某个单独的View。
2.尽量不修改现有的代码。对使用者透明。
问题最难的地方还是在于缺少一个合适的切入点来读取并且应用属性。后来使用了一些比较tricky的方式来解决这个问题:
1.已知LayoutInflater 中有一个onCreateView的方法。这个方法是public可以被复写,而且可以获取到读取xml获取到的各种属性值。因此,这是一个理想的切入点。
插一句话,虽然LayoutInflater 可以通过setFactory
之类的方式来定制View的生产,但是在这里并不合适,因为我们并不想去关注 View是如何构建出来的,只是希望在View被构建出来之后做一些额外工作。
因此希望可以替换掉系统的LayoutInfalter。
2.LayoutInflater的日常使用方式多为:
LayoutInfater.from(context)
或者Activit#getLayoutInflater
等,到最后的调用都会走到context.getSystemService(LAYOUT_INFLATER)
上来。
// LayoutInflater.java
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
而系统中的“正牌”LayoutInflate是使用的PhoneLayoutInflater
//ContextImpl.java
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
//SystemServiceRegistry
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
我们在Activity中就可以通过重写getSystemService就可以替换成我们的LayoutInflater。
public class MainActivity extends Activity {
LayoutInflater layoutInflater;
@Override
public Object getSystemService(String name) {
if (name.equals(LAYOUT_INFLATER_SERVICE)) {
if (layoutInflater == null) {
LayoutInflater origin = (LayoutInflater) super.getSystemService(name);
//使用这种 ni
layoutInflater = new PPLayoutInflater(origin, this);
}
return layoutInflater;
}
return super.getSystemService(name);
}
@Override
public LayoutInflater getLayoutInflater() {
return layoutInflater;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
- 自定义InflaterLayout的实现
这个实现参考了PhoneLayoutInflater
核心代码:
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
Log.e("shitshit", "【" + this.getClass().getSimpleName() + "】 onCreateView() called with: " + "name = [" + name + "], attrs = [" + attrs + "]");
for (String prefix : sClassPrefixList) {
try {
// 使用默认构造方法 这一步不是我们所关心的
View view = createView(name, prefix, attrs);
if (view != null) {
//do custom here...先用ImageView来做个实验
if(view instanceof ImageView) {
if (attrs != null) {
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attributeName = attrs.getAttributeName(i);
String attributeValue = attrs.getAttributeValue(i);
//拥有了View和其各种从xml中解析出来的属性
processAttrs(attributeName, attributeValue, view);
}
}
}
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
一个简单的processAttrs的栗子:
public void processAttrs(String attrName, String attrValue, View view){
if(attrName.equals("tag")){
view.setBackgroundColor(Color.parseColor(attrValue));
}
}
本文完整代码已经上传至git
https://github.com/BFridge/CustomLayoutInflater
其实关于替换SystemService的类似的小tricky也有高人专门研究过:
https://github.com/square/mortar