setContentView in Activity
getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
The setContentView method is as follows. It calls setContentView in the window, but the setContentView in the window is only an abstract method:
public abstract void setContentView(View view, ViewGroup.LayoutParams params);
At the beginning of the window class, it was also said that the only implementation class of window is PhoneWindow. Therefore, setContentView in PhoneWindow is called here, as follows:
public void setContentView(int layoutResID) { //If the parent container is null, it will be created if (mContentParent == null) { installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { //Load the incoming resource Id onto the mContentParent mLayoutInflater.inflate(layoutResID, mContentParent); } mContentParent.requestApplyInsets(); final Callback cb = getCallback(); if (cb != null && !isDestroyed()) { cb.onContentChanged(); } mContentParentExplicitlySet = true; } private void installDecor() { mForceDecorInstall = false; if (mDecor == null) { // return new DecorView(context, featureId, this, getAttributes()); //Finally, a DecorView will be created and assigned to mDecor mDecor = generateDecor(-1); //..... } else { mDecor.setWindow(this); } //mContentParent is a VeiwGroup if (mContentParent == null) { //Finally, a resource file will be loaded according to the current window style and loaded into mDecor, //And return the ViewGroup with id: @ android:id/content in the resource file, and the type is FrameLayout mContentParent = generateLayout(mDecor); } } protected ViewGroup generateLayout(DecorView decor) { // Get data from the current topic TypedArray a = getWindowStyle(); //....... // Decorate decor, judge the properties of the current window, and add the qualified layout to decor int layoutResource; int features = getLocalFeatures(); else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 && (features & (1 << FEATURE_ACTION_BAR)) == 0) { layoutResource = R.layout.screen_progress; } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) { //load resources layoutResource = R.layout.screen_custom_title; } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { layoutResource = res.resourceId; layoutResource = R.layout.screen_title; } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) { layoutResource = R.layout.screen_simple_overlay_action_mode; } else { layoutResource = R.layout.screen_simple; } mDecor.startChanging(); //Call mDecor's method and load the system layout file just found into DecorView mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); // Id_ ANDROID_ The Id of content is the Id in the resource file //Click findViewById and you can see that the View used inside is DecorView ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } //...... mDecor.finishChanging(); return contentParent; }
In the setContentView method, installDecor() is called, and this method is analyzed below.
First, a DecorView will be created, which is a View that inherits the child FrameLayout. That is, the Window class contains a DecorView.
Then it will judge whether mContentParent is null. If it is null, it will call generateLayout to create it. mContentParent is a ViewGroup. In generateLayout, system resources will be called to judge the current window mode of the system. Then load the corresponding layout. Finally, the resource file will be loaded into DecorView. And it will call findViewById to find a ViewGroup and return it. Click findViewById to see that the View used inside is DecorView. The id loaded is as follows:
Generally, the framelayout View is included in the loaded resource layout, and you can see that the id is @ android:id/content. You can copy the layout name and search globally to see the layout.
The layout is as follows:
android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" android:orientation="vertical"> <ViewStub android:id="@+id/action_mode_bar_stub" android:inflatedId="@+id/action_mode_bar" android:layout="@layout/action_mode_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="?attr/actionBarTheme" /> <FrameLayout android:id="@android:id/content" android:layout_width="match_parent" android:layout_height="match_parent" android:foregroundInsidePadding="false" android:foregroundGravity="fill_horizontal|top" android:foreground="?android:attr/windowContentOverlay" /> </LinearLayout>
So far, I've seen the more important parts of the installDecor() method. Sort it out:
Create a DecorView in setContentView(), and then obtain a resource layout according to the type of system window. Get the resource in the @ byviewid: Android control file and load it into the @ byviewid: Android control file.
Later errata: in the second box: DecorView is inherited from FrameLayout
In the setContentView method, after calling the installDecor() method, there is still a very important sentence.
mLayoutInflater.inflate(layoutResID, mContentParent);
This layoutResID is passed in when calling setContentView. This resource is loaded onto mContentParent here. Through the above analysis, we can know that contentParent is the Framelayout layout with id @ android:id/content in DecorView.
Later errata: in the second box: DecorView is inherited from FrameLayout
The final general logic is shown in the figure above
We can do a test to see if there are any problems in our analysis:
super.onCreate(savedInstanceState) // setContentView(layout()) val view = LayoutInflater.from(this).inflate(layout(), null) val decorView = window.decorView val frameLayout = decorView.findViewById<FrameLayout>(android.R.id.content) frameLayout.addView(view) }
This is the onCreate method of an activity. I annotated the setContentView method and directly loaded a layout. Then call decorView in window. Get the frameLayout and add the layout.
The effect of the final operation display is normal.
Here is a diagram that clearly shows the process of layout loading
setContentView in AppCompatActivity
In fact, there are some differences compared with setContentView of Activity. It is mainly to be compatible with some things of lower versions.
Next, let's take a look at the source code
public void setContentView(View view, ViewGroup.LayoutParams params) { getDelegate().setContentView(view, params); } @NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; } public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) { return new AppCompatDelegateImpl(activity, activity.getWindow(), callback); }
setContentView called here is also an abstract method, and the final call is in AppCompatDelegateImpl
public void setContentView(View v) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); contentParent.addView(v); mOriginalWindowCallback.onContentChanged(); } private void ensureSubDecor() { if (!mSubDecorInstalled) { mSubDecor = createSubDecor(); } } private ViewGroup createSubDecor() { TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme); final LayoutInflater inflater = LayoutInflater.from(mContext); //Empty ViewGroup ViewGroup subDecor = null; //According to a series of judgments, finally load a layout to the ViewGroup if (!mWindowNoTitle) { subDecor = (ViewGroup) inflater.inflate( R.layout.abc_dialog_title_material, null); } else if (mHasActionBar) { subDecor = (ViewGroup) LayoutInflater.from(themedContext) .inflate(R.layout.abc_screen_toolbar, null); } else { if (mOverlayActionMode) { subDecor = (ViewGroup) inflater.inflate( R.layout.abc_screen_simple_overlay_action_mode, null); } else { subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null); } } //Gets the id in the subdicor final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById( R.id.action_bar_activity_content); //The DecorView in Window is obtained here final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content); if (windowContentView != null) { //Set an empty Id windowContentView.setId(View.NO_ID); //Set a new id for contentView contentView.setId(android.R.id.content); } //Finally, the setContentView method in the window is called // Now set the Window's content view with the decor mWindow.setContentView(subDecor); return subDecor; }
Looking at the process, you can find that the setContentView in the window is finally called, but there is an additional layer here
He will first create a ViewGroup and then add a layout based on a series of judgments. Finally, call the setContentView of window. Then we will add the layout we passed in to the ViewGroup, compared with the Activity. He has an additional ViewGroup.
Compatibility of AppCompatActivity
A small example: create an activity and create an ImageView in the layout.
android:id="@+id/test_iv" android:layout_width="match_parent" android:layout_height="200dp" android:layout_marginTop="50dp" android:src="@drawable/image"/>
Then let the activity inherit the activity first, and finally AppCompatActivity, and then print the ImageView
android.widget.ImageView androidx.appcompat.widget.AppCompatImageView
It is found that if it inherits from AppcompatActivity, iamgeView finally creates AppCompatImageView, which is a compatible ImageView. Why? Let's analyze the source code:
Source code analysis:
First, a very important method is invoked in the onCreate method of AppCompatActivity, which is as follows:
protected void onCreate(@Nullable Bundle savedInstanceState) { final AppCompatDelegate delegate = getDelegate(); //The factory where View is installed. The delegate here is AppCompatDelegateImpl delegate.installViewFactory(); delegate.onCreate(savedInstanceState); super.onCreate(savedInstanceState); }
Appcompartdelegateimpl implements an interface layouteinflator Factory2
public void installViewFactory() { LayoutInflater layoutInflater = LayoutInflater.from(mContext); if (layoutInflater.getFactory() == null) { //Set the Factory of layoutinflator to this, that is, creating a View will lose its onCreateView method //If you don't understand it, take a look at the source code of LayoutInflater, LayoutInflater From (mcontext) is actually a singleton //If Factory is set, the onCreateView method will be executed first each time a View is created 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.from(mContext) is actually a singleton. He will set the Factory of layoutinflator to this.
When we're using this: layoutinflator from(this). When inputting (layout(), parent) code, it will be called into the AppCompatDelegateImpl class to implement layouteinflator onCreateView method of factory2,
@UnsupportedAppUsage(trackingBug = 122360734) @Nullable public final View tryCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { if (name.equals(TAG_1995)) { // Let's party like it's 1995! } View view; if (mFactory2 != null) { //Here, layoutinflator from(this). Inflate (layout(), null). If AppCompatActivity is used, a value will be set for mFactory2. Finally, onCreateView in AppCompatDelegateImpl will be called here. 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); } return view; } }
As you can see from the above, you are using layoutinflator from(this). How to call onCreateView in AppCompatDelegateImpl when inflate (layout(), null).
Then take a look at onCreateView
* From {@link LayoutInflater.Factory2}. */ @Override public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) { return createView(parent, name, context, attrs); } @Override public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { //....... return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed() ); }
@NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) { final Context originalContext = context; //....... View view = null; //Replaced here 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); } return view; } //Replaced AppComptImageView @NonNull protected AppCompatImageView createImageView(Context context, AttributeSet attrs) { return new AppCompatImageView(context, attrs); }
Finally, the replaced View will be returned
The important thing here is that mFactory2 will be used for the creation of View first, and then mFactory. As long as it is not null, the onCreateView method of Factory will be executed. Otherwise, the createView method of the system will be used.
LayoutInflater
It is mainly used to instantiate our layout
How to use
LayoutInflater.from(this).inflate(R.layout.activity_main,null) //2 LayoutInflater.from(this).inflate(R.layout.activity_main,null,false) //3
public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) { LayoutInflater factory = LayoutInflater.from(context); return factory.inflate(resource, root); } //2 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) { return inflate(resource, root, root != null); } //3 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { //...... }
As can be seen from the source code, the first one calls the second one, and the second one calls the third one. The third parameter is passed in according to whether the root layout is passed in or not. So we just need to lo ok at the third method
Take a look at layoutinflator from()
LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); return LayoutInflater; } ############################# ContextImpl ############ @Override public Object getSystemService(String name) { return SystemServiceRegistry.getSystemService(this, name); } //A static Map private static final Map<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS = new ArrayMap<String, ServiceFetcher<?>>(); public static Object getSystemService(ContextImpl ctx, String name) { ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name); return fetcher != null ? fetcher.getService(ctx) : null; }
You can see that what you get here is a systematic service
Then look down and you can see that this service is obtained from a static Map. So how is this Map initialized?
There is a static code block in ContextImpl, which is specially used to register various services, LAYOUT_INFLATER_SERVICE is also one of them.
From this, we can know that layoutinflator From (this) is a system service, and it is a singleton.
Next, let's see how to instantiate View
//Get resource file final Resources res = getContext().getResources(); //Parser for XmlResourceParser XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }
synchronized (mConstructorArgs) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate"); //........... //Save the transmitted Viwe View result = root; try { advanceToRootNode(parser); final String name = parser.getName(); //If it is a merge tag, call rinflete; otherwise, execute else 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"); } //Here, directly load the interface, ignore the mark, and directly pass in the root to the rinfflate to load the subview rInflate(parser, root, inflaterContext, attrs, false); } else { // Temp is the root View found in the xml and creates a View final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; // If root is not empty, set layoutParams 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); } } //Get the template first, and then transfer the template as root to rinflatchildren to load the child view behind the template rInflateChildren(parser, temp, attrs, true); //Add the View to the root layout and set the layout parameters if (root != null && attachToRoot) { root.addView(temp, params); } //... } } catch (XmlPullParserException e) { //... } return result; } } //Create View View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { //...... try { //Create View View view = tryCreateView(parent, name, context, attrs); //If no results are created if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { //Judge whether the name is the full class name, and finally create a reflection View //If not all categories, splicing is required if (-1 == name.indexOf('.')) { view = onCreateView(context, parent, name, attrs); } else { view = createView(context, name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } catch (InflateException e) { //.... } } public final View tryCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { View view; //If mFactory2 is not empty, the onCreateView of mFactory2 will be called directly // mFactory2 is set in AppCompatActivity 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); } return view; }
In AppCompatDelegateImp, the reason why it can use its own onCreateView method is that it can intercept the creation of View because it sets mFactory
If mFactory is equal to null, the View will be created by itself. If it is not null, the creation of View will be blocked and the methods in the corresponding mFactory will be executed
Next, let's look at the View creation without mFactory
public View onCreateView(@NonNull Context viewContext, @Nullable View parent, @NonNull String name, @Nullable AttributeSet attrs) throws ClassNotFoundException { return onCreateView(parent, name, attrs); } protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { //Add full class name return createView(name, "android.view.", attrs); } public final View createView(@NonNull Context viewContext, @NonNull String name, @Nullable String prefix, @Nullable AttributeSet attrs) throws ClassNotFoundException, InflateException { //Get from cache Constructor<? extends View> constructor = sConstructorMap.get(name); if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove(name); } Class<? extends View> clazz = null; try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, name); //If there is no cache, the reflection is created and added to the cache if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it clazz = Class.forName(prefix != null ? (prefix + name) : name, false, mContext.getClassLoader()).asSubclass(View.class); //Get the constructor mConstructorSignature = new Class[] {Context.class, AttributeSet.class}; //Get the constructor with two parameters constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor); } else { //....... } Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = viewContext; Object[] args = mConstructorArgs; args[1] = attrs; try { //Reflection create 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])); } return view; } finally { mConstructorArgs[0] = lastContext; } }catch (Exception e) { //...... } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }
After reading the source code, you need to know some questions
1. If you get layoutinflator
By obtaining the service of the system, and it is a single example
2. If layoutinflator is used
The three ways of use are mentioned at the beginning
3. If the layout is instantiated
The final layout is instantiated through reflection
4. Function of mFactory
Intercept the creation of View and make the creation of View follow a custom process, such as in setContentView of AppCompatView.
5. What's the difference between xml and new
The VIew creation in the layout file calls the construction of two parameters, while the direct new is the construction of one parameter. And the layout defined in xml is finally created through reflection, so try not to nest multiple layers
Intercept the creation of View
According to the above analysis, if you want to intercept the creation of View, you need to set Factory for layoutinflator.
intercept() super.onCreate(savedInstanceState) } private fun intercept() { val layoutInflater = LayoutInflater.from(this) LayoutInflaterCompat.setFactory2(layoutInflater, object : LayoutInflater.Factory2 { override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { return onCreateView(null, name, context, attrs) } override fun onCreateView( parent: View?, name: String, context: Context, attrs: AttributeSet ): View? { Log.e("BaseSkinActivity", "Intercept View Creation of") //Intercept the creation of View return if (name == "Button") { val button = Button(this@BaseSkinActivity) button.id = R.id.test_btn button.text = "intercept" return button } else null } }) }
The above set a Factory for layoutinflator to intercept the creation of VIew
In onCreateView, judge if it is a Button and modify its displayed content.
The end result is that the interception is successful.
Here, the whole article will be analyzed. If you have any questions, please point out!!!
Refer to the video from red orange Darren