Android LayoutInflater Factory

LayoutInflater.Factory简介

在之前一篇LayoutInflater的介绍中,提到 View 的 inflate 中有一个方法 createViewFromTag,会首先尝试通过 Factory 来 CreateView。

代码如下:

// these are optional, set by the caller
private boolean mFactorySet;
private Factory mFactory;
private Factory2 mFactory2;
private Factory2 mPrivateFactory;

//...

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {

    //...
    
    try {
        View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            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 {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
    }
    
    //...
}

 

今天我们来跟一下这个Factory到底是干嘛的,以及到底在什么场景使用?

先看一下Factory源码的介绍:

public interface Factory {
    /**
     * Hook you can supply that is called when inflating from a LayoutInflater.
     * You can use this to customize the tag names available in your XML
     * layout files.
     *
     * <p>
     * Note that it is good practice to prefix these custom names with your
     * package (i.e., com.coolcompany.apps) to avoid conflicts with system
     * names.
     *
     * @param name Tag name to be inflated.
     * @param context The context the view is being created in.
     * @param attrs Inflation attributes as specified in XML file.
     *
     * @return View Newly created view. Return null for the default
     *         behavior.
     */
    public View onCreateView(String name, Context context, AttributeSet attrs);
}

可以看到只有一个onCreateView的方法,参数分别是XML里边对应的Tag名字,context和attrs属性。

作用就是通过 LayoutInflater 创建View时候的一个回调,可以通过LayoutInflater.Factory来改造 XML 中存在的 Tag。

如果我们设置了LayoutInflater Factory ,在LayoutInflater 的 createViewFromTag 方法中就会通过这个 Factory 的 onCreateView 方法来创建(改变) View。

 

比如可以把一个TextView通过设置Factory后,强制转变成一个Button:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        LayoutInflater.from(this).setFactory(new LayoutInflater.Factory() {
            @Override
            public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                if(TextUtils.equals(name,"TextView")){
                    Button button = new Button(MainActivity.this);
                    button.setAllCaps(false);
                    return button;
                }
                //本来是TextView,结果create时候成了一个Button
                return getDelegate().createView(parent, name, context, attrs);
            }

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

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

(这里注意,setFactory必须放在super.onCreate()之前,稍后我们会解释原因)

 

那还有一个Factory2到底是干嘛的,跟Factory到底有什么区别和联系呢?

还是看一下源码:

public interface Factory2 extends Factory {
    /**
     * Version of {@link #onCreateView(String, Context, AttributeSet)}
     * that also supplies the parent that the view created view will be
     * placed in.
     *
     * @param parent The parent that the created view will be placed
     * in; <em>note that this may be null</em>.
     * @param name Tag name to be inflated.
     * @param context The context the view is being created in.
     * @param attrs Inflation attributes as specified in XML file.
     *
     * @return View Newly created view. Return null for the default
     *         behavior.
     */
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}

可以看到Factory2对比Factory,方法没有变化,只是多了一个参数parent。

说明Factory2可以对创建 View 的 Parent 进行控制。

LayoutInflater里边同时提供了setFactory和setFactory2方法。

 

然后我们看一下setFactory的方法:

/**
 * Attach a custom Factory interface for creating views while using
 * this LayoutInflater.  This must not be null, and can only be set once;
 * after setting, you can not change the factory.  This is
 * called on each element name as the xml is parsed. If the factory returns
 * a View, that is added to the hierarchy. If it returns null, the next
 * factory default {@link #onCreateView} method is called.
 *
 * <p>If you have an existing
 * LayoutInflater and want to add your own factory to it, use
 * {@link #cloneInContext} to clone the existing instance and then you
 * can use this function (once) on the returned new instance.  This will
 * merge your own factory with whatever factory the original instance is
 * using.
 */
public void setFactory(Factory 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 = factory;
    } else {
        mFactory = new FactoryMerger(factory, null, mFactory, mFactory2);
    }
}

/**
 * Like {@link #setFactory}, but allows you to set a {@link Factory2}
 * interface.
 */
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 {
        mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
    }
}

可以看到如果已经有设置过,mFactorySet如果为true,则直接抛异常IllegalStateException。

 

是在哪里设置的呢?

我们来找一下,在AppCompatActivity的onCreate里边找到了:

protected void onCreate(@Nullable Bundle savedInstanceState) {
    AppCompatDelegate delegate = this.getDelegate();
    //这里
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
    if (delegate.applyDayNight() && this.mThemeId != 0) {
        if (VERSION.SDK_INT >= 23) {
            this.onApplyThemeResource(this.getTheme(), this.mThemeId, false);
        } else {
            this.setTheme(this.mThemeId);
        }
    }
    super.onCreate(savedInstanceState);
}

 

然后转到AppCompatDelegateImpl里边:

public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}

如果 layoutInflater.getFactory() 为空,则会自动设置一个 Factory2。

这里调用LayoutInflaterCompat这个兼容类的setFactory2设置了Factory2:

public static void setFactory2(
        @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
    inflater.setFactory2(factory);

    if (Build.VERSION.SDK_INT < 21) {
        final LayoutInflater.Factory f = inflater.getFactory();
        if (f instanceof LayoutInflater.Factory2) {
            // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
            // We will now try and force set the merged factory to mFactory2
            forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
        } else {
            // Else, we will force set the original wrapped Factory2
            forceSetFactory2(inflater, factory);
        }
    }
}

然后才是真正调用了LayoutInflater里边的setFactory2这个方法,导致标志位mFactorySet = true。

所以,这就解释了为什么需要在super.onCreate()之前调用setFactory的原因,否则直接抛出异常。

再看一下上边这段代码的说明部分,最后会强制转为

 

为什么要在onCreate里边自动设置一个Factory呢?

其实可以通过分析代码得知,其实Factory的目的就是通过onCreateView()创建View。

所以最终,我们通过Factory中的接口找到了AppCompatDelegateImpl里边的onCreateView:

/**
 * From {@link LayoutInflater.Factory2}.
 */
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    return createView(parent, name, context, attrs);
}

public View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs) {
    if (mAppCompatViewInflater == null) {
        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
        String viewInflaterClassName =
                a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
        if ((viewInflaterClassName == null)
                || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
            // Either default class name or set explicitly to null. In both cases
            // create the base inflater (no reflection)
            
            //这里创建了一个AppCompatViewInflater
            mAppCompatViewInflater = new AppCompatViewInflater();
        } else {
            try {
                Class viewInflaterClass = Class.forName(viewInflaterClassName);
                mAppCompatViewInflater =
                        (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                                .newInstance();
            } catch (Throwable t) {
                Log.i(TAG, "Failed to instantiate custom view inflater "
                        + viewInflaterClassName + ". Falling back to default.", t);
                mAppCompatViewInflater = new AppCompatViewInflater();
            }
        }
    }

    boolean inheritContext = false;
    if (IS_PRE_LOLLIPOP) {
        inheritContext = (attrs instanceof XmlPullParser)
                // If we have a XmlPullParser, we can detect where we are in the layout
                ? ((XmlPullParser) attrs).getDepth() > 1
                // Otherwise we have to use the old heuristic
                : shouldInheritContext((ViewParent) parent);
    }

    //最后调用AppCompatViewInflater的createView
    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
            IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
            true, /* Read read app:theme as a fallback at all times for legacy reasons */
            VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
    );
}

 

继续去看一下AppCompatViewInflater的createView方法:

final View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    final Context originalContext = context;

    // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
    // by using the parent's context
    if (inheritContext && parent != null) {
        context = parent.getContext();
    }
    if (readAndroidTheme || readAppTheme) {
        // We then apply the theme on the context, if specified
        context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
    }
    if (wrapContext) {
        context = TintContextWrapper.wrap(context);
    }

    View view = null;

    // We need to 'inject' our tint aware Views in place of the standard framework versions
    switch (name) {
        case "TextView":
            view = createTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageView":
            view = createImageView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "Button":
            view = createButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "EditText":
            view = createEditText(context, attrs);
            verifyNotNull(view, name);
            break;
        case "Spinner":
            view = createSpinner(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageButton":
            view = createImageButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "CheckBox":
            view = createCheckBox(context, attrs);
            verifyNotNull(view, name);
            break;
        case "RadioButton":
            view = createRadioButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "CheckedTextView":
            view = createCheckedTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "AutoCompleteTextView":
            view = createAutoCompleteTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "MultiAutoCompleteTextView":
            view = createMultiAutoCompleteTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "RatingBar":
            view = createRatingBar(context, attrs);
            verifyNotNull(view, name);
            break;
        case "SeekBar":
            view = createSeekBar(context, attrs);
            verifyNotNull(view, name);
            break;
        default:
            // The fallback that allows extending class to take over view inflation
            // for other tags. Note that we don't check that the result is not-null.
            // That allows the custom inflater path to fall back on the default one
            // later in this method.
            view = createView(context, name, attrs);
    }

    if (view == null && originalContext != context) {
        // If the original context does not equal our themed context, then we need to manually
        // inflate it using the name so that android:theme takes effect.
        view = createViewFromTag(context, name, attrs);
    }

    if (view != null) {
        // If we have created a view, check its android:onClick
        checkOnClickListener(view, attrs);
    }

    return view;
}

@NonNull
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
    return new AppCompatTextView(context, attrs);
}

@NonNull
protected AppCompatImageView createImageView(Context context, AttributeSet attrs) {
    return new AppCompatImageView(context, attrs);
}

@NonNull
protected AppCompatButton createButton(Context context, AttributeSet attrs) {
    return new AppCompatButton(context, attrs);
}

@NonNull
protected AppCompatEditText createEditText(Context context, AttributeSet attrs) {
    return new AppCompatEditText(context, attrs);
}

@NonNull
protected AppCompatSpinner createSpinner(Context context, AttributeSet attrs) {
    return new AppCompatSpinner(context, attrs);
}

@NonNull
protected AppCompatImageButton createImageButton(Context context, AttributeSet attrs) {
    return new AppCompatImageButton(context, attrs);
}

@NonNull
protected AppCompatCheckBox createCheckBox(Context context, AttributeSet attrs) {
    return new AppCompatCheckBox(context, attrs);
}

@NonNull
protected AppCompatRadioButton createRadioButton(Context context, AttributeSet attrs) {
    return new AppCompatRadioButton(context, attrs);
}

@NonNull
protected AppCompatCheckedTextView createCheckedTextView(Context context, AttributeSet attrs) {
    return new AppCompatCheckedTextView(context, attrs);
}

 

到这里,一目了然:

设置Factory的目的就是把我们定义在xml里的View(TextView、ImageView等等),通过这个Factory工厂,转化成AppCompat前缀的View。

就是为了兼容。

 

总结:

LayoutInflater.Factory的意义:通过 LayoutInflater 创建 View 时候的一个回调,可以通过 LayoutInflater.Factory 来改造或定制创建 View 的过程。
LayoutInflater.setFactory 使用注意:不能在 super.onCreate 之后设置。
AppCompatActivity 为什么 setFactory ?向下兼容新版本中的View。

posted @ 2019-10-12 14:04  污喵王  阅读(460)  评论(0编辑  收藏  举报