最近项目中遇到一个奇怪的问题,我在同一个界面里有两个CheckBox,xml文件写的一模一样,都是使用系统默认的样式,没有改button或drawable,但是最后出来的效果居然不一样。
其实再仔细一看不止颜色不一样,未选中的样式,触摸效果都不太一样,当时瞬间就懵逼了
这是怎么回事,显然靠右那个没有带文字的CheckBox才是正常的,而RecyclerView中的不正常,虽然我觉得这个不正常的反而更好看一些呢......
log打印一下这两个CheckBox吧,结果发现
我的天呐,我分明使用的AppCompat主题,怎么出现了一个不是AppCompatCheckBoX呢,在AppCompatCheckBox的注释中分明可以看到
* This will automatically be used when you use {@link CheckBox} in your layouts.
意思是只要我开启AppCompat主题,使用CheckBox就会自动给我转成AppCompatCheckBox了,为啥没其效果?后来经过好几天不吃不喝的(弥天大雾)的研究后,我才猛的发现,虽然这两个checkBox的xml一模一样,但是获取对象的方式还是不一样的,RecyclerView中的是通过LayoutInflater获取的对象,正常的那个是findViewById获取的,果然问题出在了LayoutInflater上,冒着可能被虐到死的觉悟,我还是打开了LayoutInflater的源码一探究竟。
那么直接从inflate(int resource, ViewGroup root, boolean attachToRoot)
方法看起
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
代码比较简单,通过context创建了xml布局解析器,然后调用了inflate(parser, root, attachToRoot)
方法,接着看往下看
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
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
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
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) {
result = temp;
}
}
} catch (XmlPullParserException e) {
InflateException ex = new InflateException(e.getMessage());
ex.initCause(e);
throw ex;
} catch (Exception e) {
InflateException ex = new InflateException(
parser.getPositionDescription()
+ ": " + e.getMessage());
ex.initCause(e);
throw ex;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
}
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return result;
}
}
查看一下这个方法我简直是要放弃了(>﹏<),这么长......不过还好,我们很明显的会发现42行其中有这么一句final View temp = createViewFromTag(root, name, inflaterContext, attrs)
,很明显,view
是在这里创建的,立刻转移到这里去
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
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;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name);
ie.initCause(e);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class " + name);
ie.initCause(e);
throw ie;
}
}
在我们研究这个方法里面都做了什么之前请先看一下18行
// Let's party like it's 1995!
这是什么鬼,这是个什么梗么,谁来告诉我,这是啥
好了还是回来看这个方法吧,这个方法还好,这里会判断有没有设置Factory,会先尝试用Factory来创建view恩?这个Factory是个什么鬼?看了一眼代码它长这样
public interface Factory {
public View onCreateView(String name, Context context, AttributeSet attrs);
}
恩?一个接口,里面就一个方法,我自己创建的LayoutInflater没有设置这个Factory肯定不会走这里了,那么在我们很明显的就会发现createViewFromTag里使用Factory没有创建出view时就会调用LayoutInflater的onCreateView去创建view了
隐隐的感觉问题就出在这了,这肯定就是导致为啥我的两个xml代码一模一样的CheckBox有一个不是AppCompatCheckBox,自然这个就没法响应AppCompat主题了,更不会对colorAccent中设置的颜色有反应
那么,第二问题来了,findViewById中获得的CheckBox对象会变成AppCompatCheckBox是因为在这过程中使用了Factory所以会这样么
答案是肯定的,查看AppCompatActivity会发现
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
}
在其onCreate过程中会调用一个AppCompatDelegate的installViewFactory()
,打开后发现这是一个抽象方法
public abstract void installViewFactory();
这好办,找它的实现类,可以发现他有一个静态方法create
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV7(context, window, callback);
}
}
这里会根据不同的sdk版本返回不同的实现类,而AppCompatDelegateImplV23又是继承V14的,V14继承V11,V11继承V7,最终我们还是得跳到AppCompatDelegateImplV7里,打开其installViewFactory方法
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
if (!(LayoutInflaterCompat.getFactory(layoutInflater)
instanceof AppCompatDelegateImplV7)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
哈哈,果然如此,我们再去看看他在Factory
的onCreateView
回调里做了什么
/**
* From {@link android.support.v4.view.LayoutInflaterFactory}
*/
@Override
public final View onCreateView(View parent, String name,
Context context, AttributeSet attrs) {
// First let the Activity's Factory try and inflate the view
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}
// If the Factory didn't handle it, let our createView() method try
return createView(parent, name, context, attrs);
}
这里逻辑也很清楚,先调用callActivityOnCreateView创建view,如果没创建出来,再调用createView,先看callActivityOnCreateView
View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
// Let the Activity's LayoutInflater.Factory try and handle it
if (mOriginalWindowCallback instanceof LayoutInflater.Factory) {
final View result = ((LayoutInflater.Factory) mOriginalWindowCallback)
.onCreateView(name, context, attrs);
if (result != null) {
return result;
}
}
return null;
}
这里逻辑依然很简单,会判断一个mOriginalWindowCallback的成员变量是不是LayoutInflater.Factory类型,如果不是就会返回null,那么上面的createView就会执行,那么这个mOriginalWindowCallback又是个什么鬼呢,我们再深入看一下它,先找到它的类型,发现它是
abstract class AppCompatDelegateImplBase extends AppCompatDelegate {
final Context mContext;
final Window mWindow;
final Window.Callback mOriginalWindowCallback;
final Window.Callback mAppCompatWindowCallback;
哦,在这里找到它了,那么看看他是怎么会实例化的
AppCompatDelegateImplBase(Context context, Window window, AppCompatCallback callback) {
mContext = context;
mWindow = window;
mAppCompatCallback = callback;
mOriginalWindowCallback = mWindow.getCallback();
if (mOriginalWindowCallback instanceof AppCompatWindowCallbackBase) {
throw new IllegalStateException(
"AppCompat has already installed itself into the Window");
}
mAppCompatWindowCallback = wrapWindowCallback(mOriginalWindowCallback);
// Now install the new callback
mWindow.setCallback(mAppCompatWindowCallback);
}
这里一看就明白,主要的逻辑在wrapWindowCallback(mOriginalWindowCallback)
这个方法中,我们接着看下去
Window.Callback wrapWindowCallback(Window.Callback callback) {
return new AppCompatWindowCallbackBase(callback);
}
class AppCompatWindowCallbackBase extends WindowCallbackWrapper {
AppCompatWindowCallbackBase(Window.Callback callback) {
super(callback);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return AppCompatDelegateImplBase.this.dispatchKeyEvent(event)
|| super.dispatchKeyEvent(event);
}
public class WindowCallbackWrapper implements Window.Callback {
final Window.Callback mWrapped;
public WindowCallbackWrapper(Window.Callback wrapped) {
if (wrapped == null) {
throw new IllegalArgumentException("Window callback may not be null");
}
mWrapped = wrapped;
}
以上三段代码并不在同一处,这里为了方便一下全贴出来,可以看到最终通过这样的系列途径创建了一个callback,而AppCompatActivity最终继承自Activity,Activity实现了Window.Callback接口,最终可以说mOriginalWindowCallback应该就是AppCompatActivity对象,这里其实我也没搞太明白,纯属自己猜测,也懒得去翻官方的文档了,希望有懂的朋友指教一二
那我们再回到callActivityOnCreateView
中去
View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
// Let the Activity's LayoutInflater.Factory try and handle it
if (mOriginalWindowCallback instanceof LayoutInflater.Factory) {
final View result = ((LayoutInflater.Factory) mOriginalWindowCallback)
.onCreateView(name, context, attrs);
if (result != null) {
return result;
}
}
return null;
}
现在在回来看,这里会判断mOriginalWindowCallback instanceof LayoutInflater.Factory
,那这句会不会是true呢?答案是肯定的,因为Activity同样实现了LayoutInflater.Factory这个接口,这样就会走到if里面去,并调用Activity的onCreateView回调来创建view
那么我们再来看一下Activity是怎么实现的onCreateView
@Nullable
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
居然直接返回null,哈哈,我找了半天,进了这么多层他居然返回null,本宝宝不开森了(ToT)/~~~
既然他返回null,那么接下来代码就走到AppCompatDelegateImplV7的createView方法中去了,看看这个方法做了什么
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
final boolean isPre21 = Build.VERSION.SDK_INT < 21;
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
// We only want the View to inherit its context if we're running pre-v21
final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
isPre21, /* 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方法去处理,那么我们接着追进去看看
public 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 = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
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 it's android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
哈哈,看到这是不是拨云见日真相大白了!
注意看那一堆switch case,我想到了这了就不用我再说了吧,至此,我们终于解开了为啥使用AppCompat主题,你创建的view就会变成AppCompat的view了,完全是通过LayoutInflaterFactory来办到的,不看源码还真是不理解这里面这么精妙
本文最后让我们来解决一下最初的问题,那怎么让自己用LayoutInflater创建出的CheckBox也能响应AppCompat主题变成AppCompatCheckBox呢,非常简单,在LayoutInflater.from(context)的时候传进去AppCompatActivity的对象就行了,而我之前传的是getApplicationContext()
最后,效果如下
感谢:http://blog.csdn.net/lmj623565791/article/details/51503977 鸿洋_ 大神