诱因
之所以忽然想到LayoutInflater,是因为浏览Activity的方法时,发现了这两个家伙:
@Nullable
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (!"fragment".equals(name)) {
return onCreateView(name, context, attrs);
}
return mFragments.onCreateView(parent, name, context, attrs);
}
哎呀,好亲切的两个方法,这不和Fragment的onCreateView方法十分相似吗?难道是“如有雷同,纯属巧合”?本着刨根问底的神经,我们来追一下吧。
从Android Studio中可以看到,这两个方法来自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
接口是Api11引入的,额外加入了parent参数。Activity正是实现了该接口。
该接口是一个钩子接口,LayoutInflater在解析XML创建View时,会先调用该接口的onCreateView方法来创建,若返回null则使用默认方式创建。
Filter接口
上面介绍了Factory
和Factory2
两个接口,LayoutInflater
中还有一个公共接口:
public interface Filter {
boolean onLoadClass(Class clazz);
}
同样是钩子接口,以抛异常的处理方式过滤禁止inflate的view。
几个成员变量
//是否设置了mFactory或者mFactory2
private boolean mFactorySet;
private Factory mFactory;
private Factory2 mFactory2;
//sdk内部设置用
private Factory2 mPrivateFactory;
//过滤器
private Filter mFilter;
方法
from
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;
}
同一个context获取到的LayoutInflater相同
cloneInContext
public abstract LayoutInflater cloneInContext(Context newContext);
克隆一个新的LayoutInflater,指向newContext。newContext和原来的context可以是同一个。
getContext
public Context getContext() {
return mContext;
}
返回Context对象。
getFactory
public final Factory getFactory() {
return mFactory;
}
public final Factory2 getFactory2() {
return mFactory2;
}
获取Factory接口。
setFactory
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);
}
}
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);
}
}
这两个设置Factory的方法,都会将mFactorySet
设置为true,所以正常来说,这两个方法合计只能被调用一次。并且如果调用setFactory2
方法,则mFactory
和mFactory2
会被同时设置为factory
。
FactoryMerger
是LayoutInflater的私有内部类,实现了Factory2接口,源码如下:
private static class FactoryMerger implements Factory2 {
private final Factory mF1, mF2;
private final Factory2 mF12, mF22;
FactoryMerger(Factory f1, Factory2 f12, Factory f2, Factory2 f22) {
mF1 = f1;
mF2 = f2;
mF12 = f12;
mF22 = f22;
}
public View onCreateView(String name, Context context, AttributeSet attrs) {
View v = mF1.onCreateView(name, context, attrs);
if (v != null) return v;
return mF2.onCreateView(name, context, attrs);
}
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View v = mF12 != null ? mF12.onCreateView(parent, name, context, attrs)
: mF1.onCreateView(name, context, attrs);
if (v != null) return v;
return mF22 != null ? mF22.onCreateView(parent, name, context, attrs)
: mF2.onCreateView(name, context, attrs);
}
}
那么什么情况下才会执行创建FacotryMerger
的代码呢?正常来看应该不会,不过sdk内部有通过反射调用直接给mFactory
赋值的情况。总之对外部来讲,不必太关心FactoryMerger
。
setPrivateFactory
/**
* @hide for use by framework
*/
public void setPrivateFactory(Factory2 factory) {
if (mPrivateFactory == null) {
mPrivateFactory = factory;
} else {
mPrivateFactory = new FactoryMerger(factory, factory, mPrivateFactory, mPrivateFactory);
}
}
这是一个带有@hide
注解的方法,共sdk内部调用。
开头说到Activity实现了Factory2
接口,在其attach
方法中调用了该方法:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
//这里设置了Factory2接口
mWindow.getLayoutInflater().setPrivateFactory(this);
...
}
getFilter
public Filter getFilter() {
return mFilter;
}
setFilter
public void setFilter(Filter filter) {
mFilter = filter;
if (filter != null) {
mFilterMap = new HashMap<String, Boolean>();
}
}
设置Filter。mFilterMap起到缓存的作用,先从mFilterMap中查,查不到再调用mFilter。
inflate
共有四个重载方法,前三个方法最终都会调用到第四个方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
return inflate(parser, root, root != null);
}
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();
}
}
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)) {
//<merge>标签必须有root,并且attachToRoot为true
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
//调用rInflate
rInflate(parser, root, inflaterContext, attrs, false);
} else {//非<merge>标签
//调用createViewFromTag方法得到View
// 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 temp的子View
// 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) {
//非<merge>标签,并且root==null||!attachToRoot,方法返回值为temp,否则为root。
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(parser.getPositionDescription()
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
该方法涉及到了其他三个关键方法:rInflate,createViewFromTag,rInflateChildren
,稍后学习。关于inflate方法需要注意以下几点:
- 如果要inflate的布局根标签是
<merge>
,那么root不能为空,并且attachToRoot需要为true。 - 根布局非<merge>标签,如果root不为空,那么inflate得到的根布局的LayoutParam参数通过
root. generateLayoutParams(attrs)
得到。 - 根布局非<merge>标签,如果root==null或者attachToRoot为false,则返回值为inflate得到的根布局,否则返回root。
rInflate
r开头的表示递归方法
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)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
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);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
//inflate结束,调用parent的onFinishInflate方法
parent.onFinishInflate();
}
}
关于该函数,我们来学习if...else if...else这些分支。当name
为:
-
TAG_REQUEST_FOCUS,即
requestFocus
。
该分支会将pendingRequestFocus
变量置为true,解析结束后,调用parent.restoreDefaultFocus
方法定位焦点。如:
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="1">
<requestFocus />
</EditText>
需要注意的是,如果多个View都有<requestFocus />
标签,则最后一个获得焦点。
-
TAG_TAG,即
tag
。
这里看下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);
final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
final CharSequence value = ta.getText(R.styleable.ViewTag_value);
view.setTag(key, value);
ta.recycle();
consumeChildElements(parser);
}
最终调用了view.setTag(String, Object)
方法来设置tag。
示例如下:
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="1">
<tag android:id="@+id/tag_name"
android:value="sollian"/>
</EditText>
-
TAG_INCLUDE,即
include
。
简单看下parseInclude
方法:
private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
int type;
//<include />标签的父View应该是ViewGroup
if (parent instanceof ViewGroup) {
...
//获取<include layout="" />这个layout属性值
int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
...
if (layout == 0) {//layout属性为0,则抛出异常
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 {
...
try {
...
if (TAG_MERGE.equals(childName)) {
//处理<merge>标签
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);
//得到根View的id
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
//得到根View的可见性
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
a.recycle();
ViewGroup.LayoutParams params = null;
try {
//根据<include>标签属性生成根View的LayoutParams
params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
if (params == null) {
//根据根View的属性生成LayoutParams
params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);
// Inflate all children.
rInflateChildren(childParser, view, childAttrs, true);
if (id != View.NO_ID) {
view.setId(id);
}
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");
}
...
}
- TAG_MERGE就不用说了,<merge>必须是布局文件的根标签。
- 以上都不是,那么就是一个子View的标签。进入inflate子View的逻辑。
rInflateChildren
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
该方法构成了rInflate
方法的递归调用。inflate结束,会调用parent的onFinishInflate方法。
createViewFromTag
根据XML标签生成View,有两个重载方法:
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
...
if (name.equals(TAG_1995)) {
return new BlinkLayout(context, attrs);
}
try {
View view;
if (mFactory2 != null) {
//先尝试调用mFactory2接口生成view
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
//再尝试调用mFactory接口生成view
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
//然后尝试调用mPrivateFactory接口生成view,此处正是Activity中实现方法的调用时机。
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//name包含“.”,则认为是自定义View
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} ...
}
对name
的解析,当name
为
- “view”,则将“class”属性赋值给name。如:
<view
class="android.widget.Button"
android:layout_width="100dp"
android:layout_height="100dp" />
则该标签实际就是一个Button。
注意:此处是<view>标签,不是<View>
- TAG_1995,即“blink”,直接返回一个BlinkLayout,该类是LayoutInflater的私有静态内部类:
private static class BlinkLayout extends FrameLayout {
private static final int MESSAGE_BLINK = 0x42;
private static final int BLINK_DELAY = 500;
private boolean mBlink;
private boolean mBlinkState;
private final Handler mHandler;
public BlinkLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == MESSAGE_BLINK) {
if (mBlink) {
mBlinkState = !mBlinkState;
makeBlink();
}
invalidate();
return true;
}
return false;
}
});
}
private void makeBlink() {
Message message = mHandler.obtainMessage(MESSAGE_BLINK);
mHandler.sendMessageDelayed(message, BLINK_DELAY);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mBlink = true;
mBlinkState = true;
makeBlink();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mBlink = false;
mBlinkState = true;
mHandler.removeMessages(MESSAGE_BLINK);
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (mBlinkState) {
super.dispatchDraw(canvas);
}
}
}
该类以1s为一个周期做闪现效果。注意并不是操作的可见性,而是是否绘制。
示例:
<blink
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:src="@drawable/beauty" />
</blink>
效果为:
图片的闪现即是blink标签的效果。空白的按钮是前面提到的view标签onCreateView
有两个重载方法:
protected View onCreateView(View parent, String name, AttributeSet attrs)
throws ClassNotFoundException {
return onCreateView(name, attrs);
}
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
可以看到最终也调用了createView
,传入了android.view.
作为前缀。
createView
反射生成各个View的方法。看一下主要逻辑,省去了缓存相关:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
...
Class<? extends View> clazz = null;
try {
...
// 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);
//若设置了Filter,则判断该类是否允许inflate
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
//不允许,则抛出异常
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
...
//反射生成View
final View view = constructor.newInstance(args);
if (view instanceof 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;
} ...
}
至此,我们就学习完了LayoutInflater的源码,也弄明白了Activity中的onCreateView和Fragment的同名方法具有完全不同的作用。
那么Activity中的onCreateView可以怎么使用呢?
手动创建View
由源码可知,LayoutInflater创建View是通过反射实现的。反射存在效率低的问题,所以可以在onCreateView方法中根据name
参数直接创建View。既然可以手动创建,我们可以在XML随意写,比如:
<RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="200dp" />
然后在onCreateView中:
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if ("RecyclerView".equals(name)) {
return new RecyclerView(context, attrs);
} else {
return super.onCreateView(parent, name, context, attrs);
}
}
这个例子只是说我们可以随意定义标签名,但是并不推荐这样写,一来布局编辑器无法识别,从而给出预览页面;二来不利于维护。
更换不同的主题
可以作为一个可行性方案。
最后来学习一下android源码中的应用案例。源自appcompat-v7-27.1.1包。
看一下AppCompatActivity的部分代码:
private AppCompatDelegate mDelegate;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
//注意这个方法
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
注意到有个AppCompatDelegate.installViewFactory()
方法,该方法的实现在AppCompatDelegateImplV9
这个类中:
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
//还未设置Factory,则设置自身为Factory
if (layoutInflater.getFactory() == null) {
//AppCompatDelegateImplV9本身实现了LayoutInflater.Factory2接口,
//调用LayoutInflater.setFactory2方法来设置自身
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {//已设置Factory,则什么都不做
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
将该类设置为LayoutInflater的factory2,那么我们看下实现的onCreateView方法:
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// 首先调用Activity实现的onCreateView方法来创建
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}
// 然后调用createView方法
return createView(parent, name, context, attrs);
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return onCreateView(null, name, context, attrs);
}
上面的createView方法最终会调用AppCompatViewInflater.createView
方法:
final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
...
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;
}
看到这里有没有感觉很熟悉?该方法会将Android通用的组件替换成AppCompatXXX这些组件,虽然我们在XML文件中写的是ImageView,但实际得到的是AppCompatImageView。这样我们就可以使用api21才有的android:backgroundTint
特性了,比如:
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_back"
app:backgroundTint="#09c"/>
或者
ViewCompat.setBackgroundTintList(imageView, ColorStateList.valueof(Color.RED))
注意:在xml中书写,AS可能会有错误提示,这时不用理它,属性可以生效。
至此,我们已经知道,在继承AppCompatActivity时,我们虽然在XML文件写的是熟悉的TextView、ImageView等,但实际得到的是AppCompatTextView、AppCompatImagteView。
下面提一个小的优化点:
上面creatView的源码最后有一句:
if (view != null) {
// If we have created a view, check its android:onClick
checkOnClickListener(view, attrs);
}
checkOnClickListener用来检测View是否有android:onClick
属性,若有,则为其添加点击事件,方法实现:
private void checkOnClickListener(View view, AttributeSet attrs) {
...
final TypedArray a = context.obtainStyledAttributes(attrs, sOnClickAttrs);
final String handlerName = a.getString(0);
if (handlerName != null) {
view.setOnClickListener(new DeclaredOnClickListener(view, handlerName));
}
a.recycle();
}
可以看到,这里给View设置了DeclaredOnClickListener
,该类内部通过反射调用了我们设置的android:onClick="XXX"
中的“XXX”方法。同样的,View类内部处理该属性也是同样的方式。也就是说,使用android:onClick
属性,会创建一个中间类,然后反射调用我们设置的回调方法,效率肯定没有在java代码设置一个onClickListener高。
介绍installViewFactory
方法时,我们提到AppCompatDelegateImplV9自身实现了LayoutInflater.Factory2
,然后通过LayoutInflater.setFactory2
设置自身为View的一个创建者,并且如果我们提前设置了Factory的话,AppCompatDelegateImplV9将设置失败,这样TextView等就不会被实例化为AppCompatTextView等。
那有没有什么方式可以既使我们自定义的生效,又使AppCompatXXX生效呢?有两种简便的方式:
- 覆写Activity的onCreateView
- 按如下方式设置我们自定义的Factory
public class ActionBarActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
//必须在super.onCreate之前设置
getLayoutInflater().setFactory2(new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//添加自己的逻辑
...
//否则执行AppComaptActivity的默认逻辑
return getDelegate().createView(parent, name, context, attrs);
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
super.onCreate(savedInstanceState);
}
}
至此,LayoutInflater介绍完毕。
给出修改的demo代码:
public class ActionBarActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
getLayoutInflater().setFactory2(new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if ("image".equals(name)) {
return new AppCompatImageView(context, attrs);
} else {
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_action_bar);
ImageView vImage = findViewById(R.id.image);
ViewCompat.setBackgroundTintList(vImage, ColorStateList.valueOf(Color.GREEN));
}
public void goBack(View view) {
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<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:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ActionBarActivity">
<!-- 可以直接使用AppCompatImageView,
从而就可以在低版本api使用app:tint属性来着色 -->
<android.support.v7.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_back"
android:onClick="goBack"
app:backgroundTint="@color/colorAccent" />
<!-- 当所在Activity继承自AppCompatActivity时,
ImageView会被实例化为AppCompatImageView。
所以这里可以使用app:tint属性-->
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_back"
android:onClick="goBack"
app:backgroundTint="#90c" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_back"
android:backgroundTint="#9cc"
android:onClick="goBack" />
<!-- 百变标签,我可以是Button,也可以是TextView -->
<view
class="android.widget.Button"
android:layout_width="match_parent"
android:layout_height="100dp"
android:onClick="goBack"
android:text="button" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="1">
<requestFocus />
<!-- 设置tag,可以通过View.getTag(int)方法获取 -->
<tag
android:id="@+id/tag_name"
android:value="sollian" />
</EditText>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="2">
<!-- 后设置该属性的获取焦点-->
<requestFocus />
</EditText>
<!-- 会闪烁的FrameLayout -->
<blink
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="150dp"
android:adjustViewBounds="true"
android:src="@drawable/beauty" />
</blink>
<!-- 布局编辑器无法识别我,必须在Activity的onCreateView中手动创建我,否则报错 -->
<image
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:src="@drawable/beauty" />
</LinearLayout>
效果图:
效果图
网友评论