凑个热闹-LayoutInflater相关分析

栏目: Android · 发布时间: 5年前

内容简介:最近给组内同学做了一次“动态换肤和换文案”的主题分享,其中的核心就是官方文档我们在加载布局的时候都会主动或者被动的用到 LayoutInflater ,比如 Activity 的

最近给组内同学做了一次“动态换肤和换文案”的主题分享,其中的核心就是 LayoutInflater 类,所以把LayoutInflater源码梳理了一遍。巧了,这周掘金新榜和部分公众号都发布了LayoutInflater或者换肤主题之类的文章。那只好站在各位大佬的肩膀上,也来凑个热闹,分析一下LayoutInflater类。(前方长文预警,会有很多源码分析,源码基于Android 9.0)

LayoutInflater简介

官方文档 developer.android.com/reference/a…

我们在加载布局的时候都会主动或者被动的用到 LayoutInflater ,比如 Activity 的 setContentView 方法和Fragment的 onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) 回调等。LayoutInflater 的作用就是把布局文件xml实例化为相应的View组件。我们可以通过三种方法获取 LayoutInflater:

  1. Activity.getLayoutInflater()
  2. LayoutInflater.from(context)
  3. context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)

每个方法都和 Context 相关联,其中方法1和方法2最终都会通过方法3来实现。

获取到 LayoutInflater 后,通过调用 inflate 方法来实例化布局。而 inflate 方法由很多重载,我们常用的是 inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) ,所有 inflate 方法最终会调用到 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 。下面就从这个方法入手,开始分析 LayoutInflater 的源码。

源码分析

inflate方法

先看一下 inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 的三个参数:

  1. XmlPullParser parser:很显然是一个 XML 解析器,这个解析器就是 LayoutInflater 所要加载的 XML 布局转化来的,通过 PULL 方式解析。
  2. ViewGroup root:装载要加载的 XML 布局的根容器,比如,在 Activity 的 setContentView 方法中就是 id 为 android.R.id.content 的 FrameLayout 根布局了。
  3. boolean attachToRoot:是否将所解析的布局添加到根容器中,同时也影响了所解析布局的宽高。

被广泛讨论的是 rootattachToRoot 的不同传参对被加载的布局文件的影响,下面看代码。

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            final Context inflaterContext = mContext;
            // 将parser转成AttributeSet接口,用来读取xml中设置的View属性
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            View result = root; // 此方法返回的View,默认是root
            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
                ...
                final String name = parser.getName(); // 获取当前的标签名
                ...
                if (TAG_MERGE.equals(name)) { // 处理<merge>标签
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    // 递归处理
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    // 创建View对象
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;
                    if (root != null) {
                        ...
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs); // 获取根View的宽高
                        if (!attachToRoot) { // 如果attachToRoot为false,则给根View设置宽高
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
                    ...
                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true); // 递归处理
                    ...
                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        // 如果root不空,且attachToRoot为true,则将根View添加到容器中
                        root.addView(temp, params);
                    }
                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        // 如果root空或者attachToRoot为false,则将返回结果设置为根View
                        result = temp;
                    }
                }
            } catch (XmlPullParserException e) {
                ...
            } catch (Exception e) {
                ...
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
            // 要么是root,要么是创建的根View
            return result;
        }
    }
复制代码

从代码中可以看出 rootattachToRoot 不同传参的影响:

  1. 如果root不为null,attachToRoot设为true,则会将加载的布局添加到一个父布局中,即root,并且返回root;
  2. 如果root不为null,attachToRoot设为false,则会对布局文件最外层的所有layout属性进行设置,并且返回该布局的根View,当该view被添加到父view当中时,这些layout属性会自动生效;
  3. 如果root为null,attachToRoot将失去作用,设置任何值都没有意义,返回的也是要加载的布局的根View;

rInflate方法

从上面的方法中可以看到处理 <merge> 标签时会调用 rInflate ,处理子View时会调用 rInflateChildren 方法。其实 rInflateChildren 中调用的是 rInflate ,而 rInflate 也调用了 rInflateChildren ,从而形成了递归调用,也就是递归处理子View。

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            final String name = parser.getName();
            if (TAG_REQUEST_FOCUS.equals(name)) {
                // 处理<requestFocus>标签
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                // 处理<tag>标签
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                // 处理<include>标签
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) { // <merge>标签异常
                throw new InflateException("<merge /> must be the root element");
            } else { // 创建View对象
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true); // 递归处理孩子节点
                viewGroup.addView(view, params); // 将View添加到父布局中
            }
        }
        if (pendingRequestFocus) { // 父布局处理焦点
            parent.restoreDefaultFocus();
        }
        if (finishInflate) { // 结束加载
            parent.onFinishInflate();
        }
    }
复制代码

该方法中会处理 <requestFocus><tag><include><merge> 和普通View标签。其中:

  1. <requestFocus> 是重新定位焦点的,调用的 consumeChildElements 方法其实没干什么事,只是简单的把该标签消费结束掉。
  2. <tag> 标签一般很少用,它主要用来标记View,给View设置一个标签值,例如:
<TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" >

        <tag android:id="@+id/tag"
            android:value="hello" />

    </TextView>
    
    findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 试一下tag标签
                Toast.makeText(MainActivity.this, (String) v.getTag(R.id.tag), Toast.LENGTH_SHORT).show();
            }
        });
复制代码

在ListView的自定义Adapter中,应该都有用到过View的setTag方法,即:使用ViewHolder来重复利用View。

parseViewTag 方法:

private void parseViewTag(XmlPullParser parser, View view, AttributeSet attrs)
            throws XmlPullParserException, IOException {
        final Context context = view.getContext();
        final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewTag);
        // 读取tag的id
        final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
        // 读取tag的值
        final CharSequence value = ta.getText(R.styleable.ViewTag_value);
        // 给View设置该tag
        view.setTag(key, value);
        ta.recycle();
        // 结束该标签(子View无效)
        consumeChildElements(parser);
    }
复制代码
  1. <include> 标签不能是根标签, parseInclude 方法单独分析。
  2. <merge> 标签只能是根标签,这里会抛异常。

parseInclude方法

private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;
        if (parent instanceof ViewGroup) { // 必须在ViewGroup里才有效
            // 处理theme属性
            ...
            // If the layout is pointing to a theme attribute, we have to
            // massage the value to get a resource identifier out of it.
            // 拿到layout指定的布局
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
            ...
            if (layout == 0) { // 必须是合法的id
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                throw new InflateException("You must specify a valid layout "
                        + "reference. The layout ID " + value + " is not valid.");
            } else { // 类似于inflate的处理
                // 拿到layout的解析器
                final XmlResourceParser childParser = context.getResources().getLayout(layout);
                try {
                    final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
                    while ((type = childParser.next()) != XmlPullParser.START_TAG &&
                            type != XmlPullParser.END_DOCUMENT) {
                        // Empty.
                    }
                    if (type != XmlPullParser.START_TAG) {
                        throw new InflateException(childParser.getPositionDescription() +
                                ": No start tag found!");
                    }
                    // layout的根标签
                    final String childName = childParser.getName();
                    if (TAG_MERGE.equals(childName)) { // 处理<merge>
                        // The <merge> tag doesn't support android:theme, so
                        // nothing special to do here.
                        rInflate(childParser, parent, context, childAttrs, false);
                    } else { // 处理View
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
                        final ViewGroup group = (ViewGroup) parent;
                        final TypedArray a = context.obtainStyledAttributes(
                                attrs, R.styleable.Include);
                        // 获取<include>里设置的id
                        final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                        // 获取<include>里设置的visibility
                        final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                        a.recycle();
                        ViewGroup.LayoutParams params = null;
                        try { // 获取<include>里设置的宽高
                            params = group.generateLayoutParams(attrs);
                        } catch (RuntimeException e) {
                            // Ignore, just fail over to child attrs.
                        }
                        if (params == null) {
                            // 获取layout里设置的宽高
                            params = group.generateLayoutParams(childAttrs);
                        }
                        // <include>里设置的宽高优先于layout里设置的
                        view.setLayoutParams(params);
                        // Inflate all children.
                        rInflateChildren(childParser, view, childAttrs, true);
                        if (id != View.NO_ID) {
                            // include里设置的id优先级高
                            view.setId(id);
                        }
                        // include里设置的visibility优先级高
                        switch (visibility) {
                            case 0:
                                view.setVisibility(View.VISIBLE);
                                break;
                            case 1:
                                view.setVisibility(View.INVISIBLE);
                                break;
                            case 2:
                                view.setVisibility(View.GONE);
                                break;
                        }
                        group.addView(view);
                    }
                } finally {
                    childParser.close();
                }
            }
        } else {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }
        LayoutInflater.consumeChildElements(parser);
    }
复制代码
  1. include里必须设置layout属性,且layout的id必须合法;
  2. include里设置的id优先级高于layout里设置的id,即:两者同时设置时,后者会失效;
  3. include里设置的width和height属性优先级高于layout里设置的宽高;
  4. include里设置的visibility属性优先级高于layout设置的visibility。

createViewFromTag方法

正常View标签都是通过 createViewFromTag 来创建对应的View对象的。

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) { 
            // 真正的View标签名存在class属性中
            name = attrs.getAttributeValue(null, "class");
        }
        ...
        try {
            View view;
            if (mFactory2 != null) { // 先使用Factory2
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) { // 再使用Factory
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            if (view == null && mPrivateFactory != null) { 
                view = mPrivateFactory.onCreateView(parent, name, context, attrs);
            }
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    // 通过标签名中是否包含'.'来区分是否为自定义View
                    if (-1 == name.indexOf('.')) {
                        // 处理系统View
                        view = onCreateView(parent, name, attrs);
                    } else { // 自定义View用的是全限定类名
                        // 处理自定义View
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }
            return view;
        } catch (InflateException e) {
            ...
        } catch (ClassNotFoundException e) {
            ...
        } catch (Exception e) {
            ...
        }
    }
复制代码
  1. 优先通过Factory2和Factory来创建View,这两个Factory等会再说;
  2. 通过标签名中是否包含'.'来区分待创建的View是自定义View还是系统View;
  3. 系统View会在 onCreateView 方法中添加 android.view. 前缀,然后交由 createView 处理。

createView方法

public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        // 有缓存
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        if (constructor != null && !verifyClassLoader(constructor)) {
            constructor = null;
            sConstructorMap.remove(name);
        }
        Class<? extends View> clazz = null;
        try {
            if (constructor == null) { // 第一次则通过反射创建constructor
                // 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);
                    }
                }
                // 使用的是包含Context, AttributeSet这两个参数的构造函数
                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 = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);
                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
            }
            Object lastContext = mConstructorArgs[0];
            if (mConstructorArgs[0] == null) {
                // Fill in the context if not already within inflation.
                mConstructorArgs[0] = mContext;
            }
            Object[] args = mConstructorArgs;
            args[1] = attrs;
            // 反射创建View实例对象
            final View view = constructor.newInstance(args);
            if (view instanceof ViewStub) {
                // 如果是ViewStub则懒加载
                // Use the same context when inflating ViewStub later.
                final ViewStub viewStub = (ViewStub) view;
                viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
            }
            mConstructorArgs[0] = lastContext;
            return view;
        } 
        ...
    }
复制代码

通过反射待创建View的构造函数(两个参数:Context和AttributeSet的构造函数)来实例化View对象,如果是ViewStub对象还会进行懒加载。

LayoutInflater.Factory/Factory2

通过以上流程,使用LayoutInflater的infalte方法加载布局文件的整体流程就分析完了。但出现了 Factory2Factory 类,它们会优先创建View,我们来看看着两个类到底是什么!

它们都是LayoutInflater的内部类——两个接口:

public interface Factory {
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

    public interface Factory2 extends Factory {
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    }
复制代码

Factory2 继承了 Factory ,增加了一个带 View parent 参数的 onCreateView 重载方法。它们是在 createViewFromTag 中被调用的,默认为null,说明开发人员可以自定义这两个Factory,则通过它们可以改造待加载XML布局中的View标签,来使用自定义规则创建View。

来看一下它们的设置方法:

public void setFactory(Factory factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        // 和setFactory2类似
        ...
        }
    }

    public void setFactory2(Factory2 factory) {
        if (mFactorySet) { // 只能设置一次
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else { // 合并原有的Factory
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }
复制代码

可以看到 FactoryFactory2 只能设置一次 ,否则会抛异常。

这两个Factory的区别是什么?

  1. Factory2 是API 11 被加进来的;
  2. Factory2 继承自 Factory,也就说现在直接使用Factory2即可;
  3. Factory2 可以对创建 View 的 Parent 进行操作;

那如何应用呢?

Factory2/Factory的应用

AppCompatActivity中的应用

先看一张图:

凑个热闹-LayoutInflater相关分析

这个布局中使用的是正常的标签 <TextView><Button> ,但通过Layout Inspector工具分析页面会发现它们被替换成了 AppCompatTextViewAppCompatButton

跟踪一下 AppCompatActivityonCreate 方法:

protected void onCreate(@Nullable Bundle savedInstanceState) {
        AppCompatDelegate delegate = this.getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        ...
        super.onCreate(savedInstanceState);
    }
复制代码

委托到了 AppCompatDelegate 类,并且调用了 installViewFactory 方法。找到这个类的一个实现 AppCompatDelegateImpl (不同版本的源码这个实现类的名字不同):

class AppCompatDelegateImpl extends AppCompatDelegate implements Callback, Factory2
复制代码

看到关键的 Factory2 了,直接看 installViewFactory 方法:

public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
        if (layoutInflater.getFactory() == null) { // 设置自身到LayoutInflater
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
        }
    }
复制代码

通过 LayoutInflaterCompat.setFactory2AppCompatDelegateImpl 设置到LayoutInflater中。继续跟踪 onCreateView 的实现,会走到 createView 方法:

public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        if (this.mAppCompatViewInflater == null) {
            TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
            String viewInflaterClassName = a.getString(styleable.AppCompatTheme_viewInflaterClass);
            if (viewInflaterClassName != null && !AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
                try {
                    Class viewInflaterClass = Class.forName(viewInflaterClassName);
                    this.mAppCompatViewInflater = (AppCompatViewInflater)viewInflaterClass.getDeclaredConstructor().newInstance();
                } catch (Throwable var8) {
                    Log.i("AppCompatDelegate", "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default.", var8);
                    this.mAppCompatViewInflater = new AppCompatViewInflater();
                }
            } else {
                this.mAppCompatViewInflater = new AppCompatViewInflater();
            }
        }
        ...
        return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
    }
复制代码

会创建一个 AppCompatViewInflater 类,并且调用了它的 createView 方法,看样子找到源头了。

来到 AppCompatViewInflater 类:

public class AppCompatViewInflater {
    ...
    final View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        Context originalContext = context;
        ...
        View view = null;
        byte var12 = -1;
        switch(name.hashCode()) {
        ...
        case -938935918:
            if (name.equals("TextView")) {
                var12 = 0;
            }
            break;
        ...
        }
        switch(var12) {
        case 0:
            view = this.createTextView(context, attrs);
            this.verifyNotNull((View)view, name);
            break;
        ...
        default:
            view = this.createView(context, name, attrs);
        }
        ...
        return (View)view;
    }

    @NonNull
    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
        return new AppCompatTextView(context, attrs);
    }
    ...
}
复制代码

通过 name 参数拿到TextView标签后,直接替换成了AppCompatTextView。

通过这波操作,将一些 widget 自动变成兼容widget (例如将 TextView 变成 AppCompatTextView)以便于向下兼容新版本中的特性。

那我们也可以仿照AppCompatActivity来自定义Factory实现自己需要的替换效果。

自定义Factory2

大多换肤功能的实现就是通过实现自定义Factory,拦截特定View,然后修改这些View的属性值,或者直接返回自定义的View。举个栗子:

  1. 替换TextView的文字颜色;
  2. 在不创建selector文件前提下实现圆角按钮;

先看XML:

<LinearLayout 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"
    android:id="@+id/container"
    android:orientation="vertical"
    tools:ignore="MissingPrefix">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="10dp"
        android:paddingBottom="10dp"
        android:gravity="center"
        android:textSize="20sp"
        android:textColor="@color/third_tv_text_color"
        android:text="测试设置的Factory"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:padding="20dp"
        android:layout_gravity="center_horizontal"
        android:background="#ffbccc"
        android:text="苹果猕猴桃牛油果榴莲"
        android:textSize="15sp"
        app:cornerRadius="5dp"
        app:strokeWidth="1dp"
        app:strokeColor="#ccffcc"/>

</LinearLayout>
复制代码

注意:这里用的是系统View的标签,但属性里用到了自定义属性。

<resources>

    <declare-styleable name="MyTextView">
        <attr name="android:textColor"/>
    </declare-styleable>

    <declare-styleable name="RoundButton">
        <attr name="cornerRadius" format="dimension" />
        <attr name="strokeWidth" format="dimension" />
        <attr name="strokeColor" format="color" />
    </declare-styleable>
    
</resources>
复制代码

注意:如果想直接替换Android自带属性,需要在自定义属性里加上 android: 前缀。

public class MyFactory implements LayoutInflater.Factory2 {
    public LayoutInflater.Factory mOriginalFactory;
    // 这个模拟新资源
    private Map<String, String> mColorMap;

    public MyFactory(LayoutInflater.Factory factory) {
        this.mOriginalFactory = factory;
        mColorMap = new HashMap<>();
        // 模拟新的皮肤资源——新文字颜色
        mColorMap.put("third_tv_text_color", "#0000ff");
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        return onCreateView(name, context, attrs);
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        View view = null;
        if (mOriginalFactory != null) {
            view = mOriginalFactory.onCreateView(name, context, attrs);
        }
        if ("TextView".equals(name)) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
            // 注意这里的属性名:android:textColor,不用自定义命名空间
            int resourceId = ta.getResourceId(R.styleable.MyTextView_android_textColor, -1);
            String resourceName = context.getResources().getResourceName(resourceId);
            resourceName = resourceName.substring(resourceName.lastIndexOf('/') + 1);
            view = new TextView(context, attrs); // 可以直接修改原TextView的属性
            ta.recycle();
            
            // 这里模拟替换原TextView的textColor属性值
            String color = mColorMap.get(resourceName);
            ((TextView) view).setTextColor(Color.parseColor(color));
        }
        if ("Button".equals(name)) {
            view = new Button(context, attrs);
            // 读取自定义的属性值
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RoundButton);
            float radius = ta.getDimension(R.styleable.RoundButton_cornerRadius, 0);
            float strokeWidth = ta.getDimension(R.styleable.RoundButton_strokeWidth, 0);
            int strokeColor = ta.getColor(R.styleable.RoundButton_strokeColor, -1);
            // 构造圆角按钮
            GradientDrawable drawable = new GradientDrawable();
            drawable.setCornerRadius(DensityUtil.dip2px(context, radius));
            drawable.setStroke(DensityUtil.dip2px(context, strokeWidth), strokeColor);
            view.setBackground(drawable);
            ta.recycle();
        }
        return view;
    }
}
复制代码

注意:新资源可以通过其他方式存储和获取,从而实现动态热换肤;如果使用的是AppCompatActivity,自定义Factory必须在调用 super.onCreate 之前设置,因为它已经有了一个Factory;如果使用的是Activity,则必须在调用 setContentView 方法之前设置。

效果:

凑个热闹-LayoutInflater相关分析

可以看到TextView文字的颜色变成了蓝色;在不提供自定义drawable的xml文件以及不使用自定义View标签的前提下,实现了圆角按钮。

本文的相关示例代码都在: zjxstar的GitHub 上,感兴趣的同学可以看下。

总结

LayoutInflater的相关分析就这么多,文章有点长,慢慢看吧!

  1. LayoutInflater的inflate的过程的核心方法是:createViewFromTag 和 createView 方法;
  2. LayoutInflater通过PULL解析器来解析XML布局文件,通过反射来创建View对象;
  3. LayoutInflater.Factory只能设置一次,可以用来替换View;

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

设计之下

设计之下

搜狐新闻客户端UED团队 / 电子工业出版社 / 2014-1-1 / CNY 69.00

形而上者谓之道,形而下者谓之器。匠者,器也。处身平凡的匠人不断追求向上的设计之道。本书没有华丽的辞藻和长篇大论的理论,作者是搜狐一线的设计团队,写作过程中他们尽力还原真实的工作场景,并总结出了一些实用的经验和方法。 《设计之下》共三部分,全面讲解了用户体验设计的流程和方法。第一部分为“交互设计”,阐述了从项目启动、解析需求到原型设计的过程,并且总结了交互设计的要点:大局观、操作流程简捷、形式......一起来看看 《设计之下》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具