自定义LayoutInflator的hook点

854 阅读3分钟

实战:在不自定义控件的情况下,怎么获取这个控件的自定义属性?

先抛出问题,跟我一起复习下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;
            }
        }
    }
}

针对这个类说几点:

    1. cloneInContext(xx)必须重写的,用于创建新的LayoutInflator。
    1. "android.widget.", "android.view."有什么用? 在xml里的系统控件比如TextView等不是全路径,反射创建对象需要全路径,那这两个路径前缀是系统控件才有的,这里会在创建View时给它拼上,系统源码里面也有这个拼接的操作.
    1. 为啥这里是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 。

感谢: Android布局优化(三)使用AsyncLayoutInflater异步加载布局