關於這篇文章內容適合哪些同窗,能夠先提幾個問題請你們考慮下。java
一、若是經過一個 ImageView 類型的 view.getContext()
來獲取到的 context 是什麼類型?node
二、Activity 中調用 setContentView()
與 addContentView()
方法有什麼區別?android
三、AppCompatActivity 相對於 Activity 的 setContentView()
方法會有什麼區別麼?windows
四、Android 是如何從 XML 裏讀取並構建視圖的(或者說是如何建立一個 View 的)?markdown
五、support-v7 包裏如何針對不一樣版本 API 作到兼容和擴展的?app
六、AppCompatActivity 裏如何將一些基礎類型控件替換爲 AppCompatXXX 控件?具體哪些控件會被替換?何時替換?ide
若是對上述的問題有疑惑或者有不肯定的,均可以在下文裏找到答案。咱們會從項目調試時發現的問題入手,逐步分析找到緣由,因此本文可能會有些長,着急的小夥伴能夠按照標題找到本身關心的內容。函數
另外本文所貼的源碼版本爲:android-25 、support-v7-25.4.0佈局
爲了簡化閱讀,本文中「不相關」的代碼會有些省略,因此有須要的小夥伴能夠依照本文給的線索,自行查看全部源碼。ui
Context context = imageView.getContext();
if (context instanceof Activity) {
Activity activity = (Activity)context;
// ...
}
複製代碼
從上面的代碼舉例中能夠看到,從 imageView
控件裏獲取到 context
,轉化爲 Activity
來繼續操做。這個 imageView
是來自 XML 佈局中的一個控件,但在實際項目運行時有的手機並未走到轉換類型的 if
分支裏去,代表這個 context
並不是 Activity
類型。這個就很奇怪了,爲何呢?
/** * Simple constructor to use when creating a view from code. * * @param context The Context the view is running in, through which it can * access the current theme, resources, etc. */
public View(Context context) {
mContext = context;
//...省略
}
@ViewDebug.CapturedViewProperty public final Context getContext() {
return mContext;
}
複製代碼
咱們點進去看下 View.getContext()
方法,返回 mContext
成員變量,並且 mContext
賦值只有在構造函數裏。依據印象,這個 imageView
是寫在 XML 中的,在 setContextView(R.layout.xxx)
時候,實際調用的應該就是 PhoneWindow
裏的 setContextView()
方法,那構建使用的 context
應該就是 Activity
類型啊?
這時候我又回去仔細 Debug 了一回,發現出現問題的都是在 5.0 如下的手機裏。因此上面的印象是有問題的,在 5.0 如下,這個 imageView.getContext()
獲取到的 context
類型不是我一開始覺得的 Activity
類型,而是 TintContextWrapper
類型。
這個 TintContextWrapper 是什麼 Wrapper ?我印象中 Context 的繼承關係中沒有這個啊。 關於 Context 類型 www.jianshu.com/p/94e0f9ab3… 的講解,不清楚的小夥伴能夠自行搜索下,這裏就不展開了,網上能講清楚的也很多,這裏貼個圖看下。
確實也沒有這個 TintContextWrapper 這個類型,從名字看應該也是個 Wrapper 類型的 Context ,還和 Tint 有關係。那剩下的線索還有這個 imageView
,再 Debug 一次,發現這個 imageView
的類型也不是原先在 XML 中定義的 ImageView 類型,而是 AppCompatImageView 類型。
猛然醒悟,控件所在的 Activity 是繼承自 AppCompatActivity ,這個 context
類型的變化必定是和 v7 包裏的 AppCompatActivity 有關係。以前所謂的印象已經出了兩次錯誤,何不讀源碼解惑?
注意:下面的文章並非徹底依照查問題時的順序來的,而是閱讀完相關源碼後,整理出來的相關知識點。已經清楚的小夥伴能夠挑着閱讀。
若是屢次調用 setContentView() ,則以後每次都會清空 mContentParent
容器。而後組裝資源 layoutResID
。
若是屢次調用 addContentView() ,則以後每次都會將 View 添加到 mContentParent
容器中。最後產生 View 的疊加效果。
這個 mContentParent
存在於 PhoneWindow 中。
// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
ViewGroup mContentParent;
複製代碼
setContentView() 方法有兩類,其中一類的必要參數是 XML 佈局 id ,另外一類的必要參數是 View 類型。
setContentView(@LayoutRes int layoutResID)
setContentView(View view)
這裏咱們以參數爲 View 類型的代碼討論。
// Activity代碼
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
public Window getWindow() {
return mWindow;
}
複製代碼
Activity 中 setContentView()
代碼,獲取 window
來 setContentView()
。
// Window代碼
public abstract void setContentView(View view);
複製代碼
而這個 window
其實就是 PhoneWindow
,看下面的代碼。
// Activity代碼
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) {
//...省略
mWindow = new PhoneWindow(this, window);
//...省略
}
複製代碼
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
//...省略
}
複製代碼
代碼第12行,確保 mContentParent
已經初始化過。
第14行,若是沒有 FEATURE_CONTENT_TRANSITIONS
,先清空 mContentParent
裏內容。
第22行, mContentParent
將 view
當子孩子添加進來。
第17行,若是有 FEATURE_CONTENT_TRANSITIONS
,調用 transitionTo(newScene)
。這部分不展開了,最終也是調用如下代碼,邏輯步驟都是同樣的。
//Scene 代碼
//mSceneRoot 就是剛纔的 mContentParent
//mLayout 就是 setContentView 方法傳進來的 view
public Scene(ViewGroup sceneRoot, View layout) {
mSceneRoot = sceneRoot;
mLayout = layout;
}
public void enter() {
// Apply layout change, if any
if (mLayoutId > 0 || mLayout != null) {
// empty out parent container before adding to it
getSceneRoot().removeAllViews();
if (mLayoutId > 0) {
LayoutInflater.from(mContext).inflate(mLayoutId, mSceneRoot);
} else {
mSceneRoot.addView(mLayout);
}
}
//...省略
}
複製代碼
能夠看到 Activity 中 setContentView() 流程仍是比較簡單的,基本上就是調用了PhoneWindow 裏的相應方法。下面咱們來看看 AppCompatActivity 中有什麼特別的。
// AppCompatActivity
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
/** * @return The {@link AppCompatDelegate} being used by this Activity. */
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
複製代碼
mDelegate
是一個代理類,由 AppCompatDelegate 根據不一樣的 SDK 版本生成不一樣的實際執行類,就是個代理的兼容模式。看下面的代碼:
/** * Create a {@link android.support.v7.app.AppCompatDelegate} to use with {@code activity}. * * @param callback An optional callback for AppCompat specific events */
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return create(activity, activity.getWindow(), callback);
}
private static AppCompatDelegate create(Context context, Window window, AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else 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 AppCompatDelegateImplV9(context, window, callback);
}
}
複製代碼
咱們能夠看到最基礎的就是 AppCompatDelegateImplV9 這個版本,其餘的實現類最終都是繼承自這個 AppCompatDelegateImplV9 類的。咱們後面要查看的方法都在 AppCompatDelegateImplV9 這個類實現裏。
因此咱們在 AppCompatActivity 中調用 setContentView()
方法,實際最終實現都是 AppCompatDelegateImplV9 裏。
// 代理類的具體實現類 AppCompatDelegateImplV9 中 setContentView() 方法
@Override
public void setContentView(View v, ViewGroup.LayoutParams lp) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v, lp);
mOriginalWindowCallback.onContentChanged();
}
複製代碼
從代碼第 5 - 7 行,從 mSubDector
(類型 ViewGroup )中取出個 android.R.id.content
標識的 contentParent
,而後從新添加 view
。第 8 行回調通知。
那第 4 行代碼從名字上能夠看出是確保這個 mSubDector
初始化的方法。咱們進去看下:
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
//...省略...
}
}
複製代碼
private ViewGroup createSubDecor() {
//...省略... 這部分主要針對 AppCompat 樣式檢查和適配
// Now let's make sure that the Window has installed its decor by retrieving it
mWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
//...省略... 這部分主要針對不一樣的樣式設置來初始化不一樣的 subDecor(inflater 不一樣的佈局 xml )
if (subDecor == null) {
throw new IllegalArgumentException(
"AppCompat does not support the current theme features: { "
+ "windowActionBar: " + mHasActionBar
+ ", windowActionBarOverlay: "+ mOverlayActionBar
+ ", android:windowIsFloating: " + mIsFloating
+ ", windowActionModeOverlay: " + mOverlayActionMode
+ ", windowNoTitle: " + mWindowNoTitle
+ " }");
}
//...省略...
// Make the decor optionally fit system windows, like the window's decor
ViewUtils.makeOptionalFitsSystemWindows(subDecor);
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
// There might be Views already added to the Window's content view so we need to
// migrate them to our content view
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
// Change our content FrameLayout to use the android.R.id.content id.
// Useful for fragments.
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);
// The decorContent may have a foreground drawable set (windowContentOverlay).
// Remove this as we handle it ourselves
if (windowContentView instanceof FrameLayout) {
((FrameLayout) windowContentView).setForeground(null);
}
}
// Now set the Window's content view with the decor
mWindow.setContentView(subDecor);
//...省略...
return subDecor;
}
複製代碼
下面咱們重點看一下代碼 28 - 31 行,從 subDecor
中取出了 R.id.action_bar_activity_content
標示的 FrameLayout ,從 window
中取出咱們熟悉的 android.R.id.content
標示 view
。這個 view
呢其實就是 PhoneWindow 中 DecorView 裏的 contentView
了。
代碼 35 - 38 行,就是將 window
裏取出的 windowContentView
裏已有的 childview 依次挪到這個 subDector
取出的 contentView
中去,並清空這個 windowContentView
。這裏就達到狸貓換太子的第一步。
代碼 43 - 44 行,接下來將原來 window
裏的 windowContentView
的 id( android.R.id.content )替換給咱們 subDecor
裏的 contentView
。
代碼 54 行,狸貓換太子的最後一步,將狸貓 subDecor
設置給 mWindow
。
分析完上述代碼,咱們再回過來看一下 setContentView() 方法的代碼第 4 行,就不難理解爲何能夠經過 android.R.id.content
來取到 「根 View 」 了。
@Override
public void setContentView(View v, ViewGroup.LayoutParams lp) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v, lp);
mOriginalWindowCallback.onContentChanged();
}
複製代碼
剛纔咱們討論了一類參數爲 View 的 setContentView() 方法,如今咱們來看下另外一個參數爲佈局 id 的 setContentView() 方法。
當咱們在 Activity 的 onCreate() 方法裏調用 setContentView(R.layout.xxx) 來設置一個頁面時,最終都會走到相似以下的方法:
LayoutInflater.from(mContext).inflate(resId, contentParent);
因此下面咱們來看下怎麼 inflate 一個頁面出來。
// LayoutInflater 代碼
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, 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();
}
}
複製代碼
看代碼第 13 行,經過 XML 解析器 XmlResourceParser 來解析咱們傳進來的佈局文件的。下面咱們貼下第 14 行代碼方法的詳細。
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) {
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;
}
}
複製代碼
能夠看到上面的代碼不是特別多,主要就是根據一個個 XML 中的標籤( </> 封裝的內容),用 parser
來解析並作相應處理。
代碼第 74 行將 view 添加到 root
中去。而這個 root
就是一開始傳下來的 contentParent
(類型 ViewGroup )。
那就有疑問了,讀取到標籤,知道是什麼標籤了,好比是個 TextView ,那在什麼地方建立一個 View 呢?
代碼第 41 - 42 行,調用 createViewFromTag()
方法來建立 View 的。
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
咱們簡化掉一部分代碼。
// LayoutInflater 代碼
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;
} catch
//...省略捕獲異常...
}
複製代碼
其中 Factory
、 Factory2
都是接口,都提供了 onCreateView()
方法,其中 Factory2
繼承自 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);
}
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);
}
複製代碼
若是全部 factory 都爲空或者 factory 構建的 view 爲空,則最終調用 CreareView()
方法了,關於此方法代碼就不貼了,就是經過控件名字( XML 中標籤名)反射生成個對象,貼一段註釋就明白了。
Low-level function for instantiating a view by name. This attempts to instantiate a view class of the given name found in this LayoutInflater's ClassLoader.
最後的疑問就是這個 Factory(或 Factory2 )接口類型的成員變量何時會賦值了?請往下看。
咱們先看看 Activity 是實現了 LayoutInflater.Factory2 接口的。
public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2, Window.Callback, KeyEvent.Callback, OnCreateContextMenuListener, ComponentCallbacks2, Window.OnWindowDismissedCallback, WindowControllerCallback {
//...省略
/** * Standard implementation of * {@link android.view.LayoutInflater.Factory#onCreateView} used when * inflating with the LayoutInflater returned by {@link #getSystemService}. * This implementation does nothing and is for * pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} apps. Newer apps * should use {@link #onCreateView(View, String, Context, AttributeSet)}. * * @see android.view.LayoutInflater#createView * @see android.view.Window#getLayoutInflater */
@Nullable
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
/** * Standard implementation of * {@link android.view.LayoutInflater.Factory2#onCreateView(View, String, Context, AttributeSet)} * used when inflating with the LayoutInflater returned by {@link #getSystemService}. * This implementation handles <fragment> tags to embed fragments inside * of the activity. * * @see android.view.LayoutInflater#createView * @see android.view.Window#getLayoutInflater */
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」
。若是咱們的 XML 中用 fragment 標籤來嵌入一個 Fragment ,在解析 XML 時候,會在 Activity 中調用 mFragments
的 onCreateView()
方法來返回一個 View ,最後加入到 contentParent
中。
// Activity 代碼
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) {
//...省略
mWindow = new PhoneWindow(this, window);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
複製代碼
仍是這個 attach()
方法( Internal API ),在代碼第 15 行調用了 PhoneWindow 的 getLayoutInflater()
方法,設置了 privateFactory
。
public PhoneWindow(Context context) {
super(context);
mLayoutInflater = LayoutInflater.from(context);
}
/** * Return a LayoutInflater instance that can be used to inflate XML view layout * resources for use in this Window. * * @return LayoutInflater The shared LayoutInflater. */
@Override
public LayoutInflater getLayoutInflater() {
return mLayoutInflater;
}
複製代碼
代碼已經說明了一切,註釋也很清楚了。
請往下看
咱們以前的內容都是一些準備知識,咱們最初的問題是 ImageView 裏 getContext() 的類型爲何在 5.0 如下會是 TintContextWrapper ?何時以及是替換掉的?尚未解答,下面會陸續給出答案。小夥伴們堅持下!
@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);
}
複製代碼
怎麼樣第 3 行代碼是否是很熟悉,代理加兼容模式,這個 AppCompatDelegate 具體實現類咱們再看一遍。
// AppCompatActivity 代碼,代碼 8 行的 this 就是這個 Activity 自己。
/** * @return The {@link AppCompatDelegate} being used by this Activity. */
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
// AppCompatDelegate代碼
private static AppCompatDelegate create(Context context, Window window, AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
return new AppCompatDelegateImplN(context, window, callback);
} else 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 AppCompatDelegateImplV9(context, window, callback);
}
}
複製代碼
AppCompatActivity.onCreate() 代碼裏,第 4 行 delegate.installViewFactory()
。具體的實現是在 AppCompatDelegateImplV9 裏。看以下代碼:
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
if (!(LayoutInflaterCompat.getFactory(layoutInflater)
instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
複製代碼
代碼第 3 - 5 行,若是 layoutInflater
的factory
爲空,則將自身設置給layoutInflater
,達到設置 factory 的效果( 4.3 章節問題解決),也達到了自定義 contentView 的效果。
對比下以前的 setContentView(View view) 代碼,有區別就是在下面的第 6 行。
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
複製代碼
還不明白 AppCompatActivity 如何自定義 contentView 的小夥伴,能夠回去看看第四章,看看 4.2 createViewFromTag() 方法 章節。對 contentParent
有疑問的看看第三章。
聯繫下咱們最初的問題,在這裏傳給 LayoutInflater 的 mContext
已經替換TintContextWrapper 了麼?固然不是,從 AppCompatActivity.onCreate() 方法裏一路傳下來的 context
都是 AppCompatActivity 自身。咱們還得往下看。
從 5.1 的代碼咱們已經能夠看到在 AppCompatActivity 中經過 AppCompatDelegateImplV9 將本身與 LayoutInflater 的 setFactory 系列方法關聯。具體實現 Factory 接口方法也天然在 AppCompatDelegateImplV9 中了。
這裏咱們先將 support-v4 包裏 LayoutInflaterFactory 接口等同與 LayoutInflater 的 Factory2 接口,具體如何等效咱們後面第 6 章節會講述。
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase implements MenuBuilder.Callback, LayoutInflaterFactory {
//...省略...
/** * 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);
}
//...省略...
@Override
public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
//...省略...
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 */
);
}
//...省略...
}
複製代碼
從上面的代碼能夠看到,LayoutInflate
裏 Factory2 接口 onCreateView()
方法的實現,是在 AppCompatDelegateImplV9 ( AppCompatActivity 中代理實現類)中而且使用的是 AppCompatViewInflater
。忘記了能夠回去看看第四章。
咱們再進去看看這個 AppCompatViewInflater 的 createView()
是作了什麼事情。
「duang duang duang」!
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;
}
複製代碼
代碼 15 - 17 行,若是 wrapContext
爲 true ,將 context
用 TintContextWrapper
包了一次。咱們終於第一次看到這個 TintContextWrapper
了!!!下面咱們再詳細看。
代碼 23 - 61 行,將一些常見的基礎 View 轉變爲 AppCompatXXX 了。終於知道在 AppCompatActivity 中哪些基礎控件會被替換了,具體參見上面的 case 。
代碼 23 - 61 行,將一些常見的基礎 View 轉變爲 AppCompatXXX 了。終於知道在 AppCompatActivity 中哪些基礎控件會被替換了,具體參見上面的 case 。
這裏咱們只看下 AppCompatImageView 的構造函數(其餘相似),也將 context
用 TintContextWrapper
包下。
public AppCompatImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AppCompatImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
//...省略...
}
複製代碼
代碼直接告訴咱們 SDK 版本低於 21 ( android 5.0 ),將 Context 包裝成 TintContextWrapper 類型。 這就是爲何 XML 中的 ImageView 獲取到的 Context 多是 TintContextWrapper 類型了。
public static Context wrap(@NonNull final Context context) {
if (shouldWrap(context)) {
synchronized (CACHE_LOCK) {
//...省略...
// If we reach here then the cache didn't have a hit, so create a new instance
// and add it to the cache
final TintContextWrapper wrapper = new TintContextWrapper(context);
//...省略...
return wrapper;
}
}
return context;
}
private static boolean shouldWrap(@NonNull final Context context) {
if (context instanceof TintContextWrapper
|| context.getResources() instanceof TintResources
|| context.getResources() instanceof VectorEnabledTintResources) {
// If the Context already has a TintResources[Experimental] impl, no need to wrap again
// If the Context is already a TintContextWrapper, no need to wrap again
return false;
}
return Build.VERSION.SDK_INT < 21 || VectorEnabledTintResources.shouldBeUsed();
}
複製代碼
不管是在 5.2 章節裏 mAppCompatViewInflater.createView()
方法裏仍是 TintContextWrapper.shouldWrap()
方法裏都有這句 VectorEnabledTintResources.shouldBeUsed() 。咱們繼續看下代碼:
@RestrictTo(LIBRARY_GROUP)
public class VectorEnabledTintResources extends Resources {
public static boolean shouldBeUsed() {
return AppCompatDelegate.isCompatVectorFromResourcesEnabled()
&& Build.VERSION.SDK_INT <= MAX_SDK_WHERE_REQUIRED;
}
/** * The maximum API level where this class is needed. */
public static final int MAX_SDK_WHERE_REQUIRED = 20;
//...省略...
}
複製代碼
//AppCompatDelegate代碼
//...省略...
private static boolean sCompatVectorFromResourcesEnabled = false;
//...省略...
/** * Sets whether vector drawables on older platforms (< API 21) can be used within * {@link android.graphics.drawable.DrawableContainer} resources. * * <p>When enabled, AppCompat can intercept some drawable inflation from the framework, which * enables implicit inflation of vector drawables within * {@link android.graphics.drawable.DrawableContainer} resources. You can then use those * drawables in places such as {@code android:src} on {@link android.widget.ImageView}, * or {@code android:drawableLeft} on {@link android.widget.TextView}. Example usage:</p> * * <pre> * <selector xmlns:android="..."> * <item android:state_checked="true" * android:drawable="@drawable/vector_checked_icon" /> * <item android:drawable="@drawable/vector_icon" /> * </selector> * * <TextView * ... * android:drawableLeft="@drawable/vector_state_list_icon" /> * </pre> * * <p>This feature defaults to disabled, since enabling it can cause issues with memory usage, * and problems updating {@link Configuration} instances. If you update the configuration * manually, then you probably do not want to enable this. You have been warned.</p> * * <p>Even with this disabled, you can still use vector resources through * {@link android.support.v7.widget.AppCompatImageView#setImageResource(int)} and it's * {@code app:srcCompat} attribute. They can also be used in anything which AppCompat inflates * for you, such as menu resources.</p> * * <p>Please note: this only takes effect in Activities created after this call.</p> */
public static void setCompatVectorFromResourcesEnabled(boolean enabled) {
sCompatVectorFromResourcesEnabled = enabled;
}
/** * Returns whether vector drawables on older platforms (< API 21) can be accessed from within * resources. * * @see #setCompatVectorFromResourcesEnabled(boolean) */
public static boolean isCompatVectorFromResourcesEnabled() {
return sCompatVectorFromResourcesEnabled;
}
複製代碼
那何時 VectorEnabledTintResources.shouldBeUsed()
返回 true ?當版本低於 5.0 且調用 AppCompatDelegate.setCompatVectorFromResourcesEnabled
設置爲 true (注意是靜態方法)。
這個 VectorEnabledTintResources.shouldBeUsed()
方法實際上是判斷當系統在 5.0 如下時,是否要支持矢量圖資源,默認 false 。對這塊有疑惑的同窗,能夠搜索相關的矢量圖使用方法,兼容低版本策略,這裏就不展開了。
一、在 AppCompatActivity 中,onCreate()
方法裏先創建了本身的代理實現類,該類實現了 LayoutInflater.Fatory2 接口(實際上是 support-v4 包裏的 LayoutInflaterFactory 接口)。
二、再調用 installViewFactory()
方法,將代理實現類和 LayoutInflater 裏的 factory
成員變量綁定。
三、當咱們本身調用 setContentView(R.layout.xxx)
方法後,解析 XML 時會調用到 LayoutInflater 裏的 inflate()
方法,再接着是 createViewFromTag()
方法。
四、createViewFromTag()
方法裏若是有 factory 系列的本地變量,就先調用這些接口的 onCreateView()
方法。在 AppCompatActivity 中 onCreateView()
是在 AppCompatDelegateImplV9 裏。
五、AppCompatDelegateImplV9 裏用 AppCompatViewInflater
來生成 View。因此有了替換基礎控件的內容,有了 5.0 如下系統將 Context
包裝成TintContextWrapper
,構建 AppCompatxxx 控件時,傳入的 context
被替換成了 TintContextWrapper
類型。
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
//...省略...
}
}
複製代碼
最後的疑問了:代碼第 5 行,如何將 layoutInflater
接受的Factory(Factory2)類型變爲接受 this(實現了 android.support.v4.view.LayoutInflaterFactory 接口)??
先看下 v4 包裏關於 LayoutInflaterFactory 的註釋,能夠明白其意圖。如何實現這樣的目的,咱們往下看 6.1 章。
/** * Used with {@code LayoutInflaterCompat.setFactory()}. Offers the same API as * {@code LayoutInflater.Factory2}. */
public interface LayoutInflaterFactory {
/** * 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. * * @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);
}
複製代碼
咱們回到 android.support.v4.view.LayoutInflaterCompat 裏看作了什麼。
// 代碼android.support.v4.view.LayoutInflaterCompat
/** * 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. * * @see LayoutInflater#setFactory(android.view.LayoutInflater.Factory) */
public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
IMPL.setFactory(inflater, factory);
}
static final LayoutInflaterCompatImpl IMPL;
static {
final int version = Build.VERSION.SDK_INT;
if (version >= 21) {
IMPL = new LayoutInflaterCompatImplV21();
} else if (version >= 11) {
IMPL = new LayoutInflaterCompatImplV11();
} else {
IMPL = new LayoutInflaterCompatImplBase();
}
}
複製代碼
又是咱們熟悉的代理模式,實現類 IMP
又是一個兼容模式。
咱們看一個最簡單的 LayoutInflaterCompatBase
的代碼實現就明白了。
//代碼LayoutInflaterCompat
interface LayoutInflaterCompatImpl {
public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory);
public LayoutInflaterFactory getFactory(LayoutInflater layoutInflater);
}
static class LayoutInflaterCompatImplBase implements LayoutInflaterCompatImpl {
@Override
public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) {
LayoutInflaterCompatBase.setFactory(layoutInflater, factory);
}
@Override
public LayoutInflaterFactory getFactory(LayoutInflater layoutInflater) {
return LayoutInflaterCompatBase.getFactory(layoutInflater);
}
}
複製代碼
class LayoutInflaterCompatBase {
static class FactoryWrapper implements LayoutInflater.Factory {
final LayoutInflaterFactory mDelegateFactory;
FactoryWrapper(LayoutInflaterFactory delegateFactory) {
mDelegateFactory = delegateFactory;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return mDelegateFactory.onCreateView(null, name, context, attrs);
}
public String toString() {
return getClass().getName() + "{" + mDelegateFactory + "}";
}
}
static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
inflater.setFactory(factory != null ? new FactoryWrapper(factory) : null);
}
//...省略...
}
複製代碼
代碼第 22 行,將 v4 包裏的 LayoutInflaterFactory 包裝成 FactoryWrapper 類型,再調用 LayoutInflater 的 setFactory()
方法。
代碼 13 行,運用代理模式。FactoryWrapper 實現了 LayoutInflater 的 Factory 接口,在具體的 onCreateView()
方法實現中替換爲代理類來實現。
代碼第 7 行,FactoryWrapper 的構造函數入參就是個代理類,類型正是 v4 包裏的 LayoutInflaterFactory 接口。
一、在 LayoutInflaterCompat.setFactory(layoutInflater, this);
裏,經過一系列的代理兼容模式,將 LayoutInflater 的 setFactory()
系列方法接收的參數,變化爲 v4 包裏的 LayoutInflaterFactory 接口類型參數。
二、傳入的 this
就是 AppCompatDelegateImplV9 自己。因此 Factory 系列接口的 onCreateView()
方法實現,就落到了 AppCompatDelegateImplV9 裏的方法裏。
一、問題 View.getContext() 如何強制轉爲 Activity ?
下面給個經常使用思路做爲參考:
@Nullable
private Activity getActivity(@NonNull View view) {
if (null != view) {
Context context = view.getContext();
while (context instanceof ContextWrapper) {
if (context instanceof Activity) {
return (Activity) context;
}
context = ((ContextWrapper) context).getBaseContext();
}
}
return null;
}
複製代碼
首先這篇文章貼的源碼有點多,分析的內容也只是源碼中的一部分。再加上這麼長的內容,並無一個很好的敘述順序將這些內容有趣味性地串起來。因此堅持下來的小夥伴們,給大家點個贊!
本文原由來自一個須要修復的項目 bug ,後來在團隊內的技術交流會中分享後,從新完善寫下來的。文字對比現場講解仍是少了些互動交流,因此有寫得疏漏地方和錯誤地方,請你們不吝指教。謝謝了!
歡迎留言或者發郵件給我:fanzhu@imdada.cn