Here’s a smooth, step-by-step introduction to some of Factory2’s quirks and tricks from 2018 that’s worth a read.
By Ahmed El-Helw
Original address: helw.net/2018/08/06/…
When we create a new Activity, we mostly inherit AppCompatActivity. For Android developers, it provides backward compatibility that greatly simplifies our development. But how does it work? In particular, how does it replace TextView in the XML layout file with AppCompatTextView?
This article will delve into the view loading process for AppCompatActivity.
Factory2
In Android, we often write layouts in XML files. These files are packaged into the app (converted to binary XML by AAPT /2 for performance reasons) and loaded by LayoutInflater at runtime.
There are two methods in LayoutInflater, setFactory and setFactory2, described in the documentation like this:
When creating views using LayoutInflaters, bind a custom Factory instance. Cannot be null, and can only be set once, after which it cannot be changed, when each element name in the XML is parsed. If the Factory returns a View, it is added to the View hierarchy. If null is returned, the factory’s next default method onCreateView(View, String, AttributeSet) is called.
Note that Factory2 implements Factory, so setFactory2 should be used for Api 11+ applications. This gives us the opportunity to intervene in the creation of every View element in XML. Let’s look at a practical use:
class FactoryActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?). {
val layoutInflater = LayoutInflater.from(this)
layoutInflater.factory2 = object : LayoutInflater.Factory2 {
override fun onCreateView(parent: View? , name:String,
context: Context,
attrs: AttributeSet): View? {
// Replace TextView with RedTextView
if (name == "TextView") {
return RedTextView(context, attrs)
}
return null
}
override fun onCreateView(name: String,
context: Context,
attrs: AttributeSet): View? {
return onCreateView(null, name, context, attrs)
}
}
super.onCreate(savedInstanceState)
setContentView(R.layout.factory)
}
Copy the code
In the code above, we just set a Factory2 for the LayoutInflater of the current Context. So whenever we find a TextView, we’ll replace it with our own implementation class, RedTextView.
RedTextView is a subclass of TextView that provides the setBackgroundColor method to set the background to red:
class RedTextView : AppCompatTextView {
constructor(context: Context) : super(context) { initialize() }
constructor(context: Context, attrs: AttributeSet?) :
super(context, attrs) { initialize() }
constructor(context: Context, attr: AttributeSet? , defStyleAttr:Int) :
super(context, attr, defStyleAttr) { initialize() }
private fun initialize(a) { setBackgroundColor(Color.RED) }
}
Copy the code
The layout file factory.xml looks like this:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Hello" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="World" />
</LinearLayout>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Welcome" />
</LinearLayout>
Copy the code
Running the application and using the Layout Inspector, we see that all textViews become RedTextViews. Great!
AppcompatActivity and Factory2
If we change the FactoryActivity above to inherit AppCompatActivity, we can see that TextView does become RedTextView. But the Button we add is still a Button and has not become an AppCompatButton, why is that?
The first two lines of the onCreate method for AppCompatActivity are:
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
Copy the code
GetDelegate () depending on the API version returns the corresponding proxy class (AppCompatDelegateImplV14, AppCompatDelegateImplV23 AppCompatDelegateImplN, etc.).
The next line delegate. InstallViewFactory () :
public void installViewFactory(a) {
LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else if(! (layoutInflater.getFactory2()instanceof AppCompatDelegateImpl)) {
Log.i("AppCompatDelegate"."The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's"); }}Copy the code
When the layoutInflater. GetFactory () is null, invoked setFactory2. If it is not empty, nothing will be done.
So the reason the Button doesn’t change is that the Factory has already been set, causing the AppcompatActivity’s own Factory not to be installed.
Note that the setFactory2() method of FactoryActivity is called before super.oncreate. If not, setFactory2 will throw an exception when the parent is AppcompatActivity. Because AppCompatActivity sets its Factory. It cannot be empty and can only be set once; You cannot change the Factory after setting it.
How can Factory2 be compatible with AppCompatActivity
How can AppCompatActivity keep its Facotory while using its own Factory2? Here are a few solutions.
The agent toAppCompatDelegate
There is a createView method inside AppCompatDelegate, not to be confused with Factory, Factory2’s onCreateView.
/**
* This should be called from a
* {@link android.view.LayoutInflater.Factory2 LayoutInflater.Factory2}
* in order to return tint-aware widgets.
* <p>
* This is only needed if you are using your own
* {@link android.view.LayoutInflater LayoutInflater} factory, and have
* therefore not installed the default factory via {@link #installViewFactory()}.
*/
public abstract View createView(@Nullable View parent,
String name,
@NonNull Context context,
@NonNull AttributeSet attrs);
Copy the code
We just need to modify setFactory2 to delegate anything that doesn’t need to be processed to AppCompatDelegate:
override fun onCreate(savedInstanceState: Bundle?) {
val layoutInflater = LayoutInflater.from(this)
layoutInflater.factory2 = object : LayoutInflater.Factory2 {
override fun onCreateView(parent: View? , name: String, context: Context, attrs: AttributeSet): View? {
if (name == "TextView") {
return RedTextView(context, attrs)
}
// Proxy to AppCompatActivity's getDelegate()
return delegate.createView(parent, name, context, attrs)
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return onCreateView(null, name, context, attrs)
}
}
super.onCreate(savedInstanceState)
setContentView(R.layout.factory)
}
Copy the code
TextView becomes RedTextView and Button becomes AppCompatButton.
rewriteviewInflaterClass
Let’s look at the createView method of AppCompatDelegate, which is created by reflection when AppCompatViewInflater is not initialized. To initialize the class by R.s tyleable. AppCompatTheme_viewInflaterClass specified, the default is AppCompatViewInflater.
Change the FactoryActivity theme as follows:
<style name="FactoryTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="viewInflaterClass">com.cafesalam.experiments.app.ui.CustomViewInflater</item> </style>
Copy the code
We can ask an AppCompatDelegate to use our custom subclass of AppCompatViewInflater, CustomViewInflater:
class CustomViewInflater : AppCompatViewInflater(a){
override fun createTextView(context: Context, attrs: AttributeSet) =
RedTextView(context, attrs)
}
Copy the code
Google’s Material Design Components actually use this method to modify the Button to the corresponding MaterialButton, as you can see here.
This method is powerful and allows your App to use libraries such as Material Design Components, but only by setting the appropriate theme.
Note that AppCompatViewInflater also provides a createView() method that can be overridden to process new components that are not processed by default. This method can be used when AppCompatViewInflater does not process a specific component type.
Custom LayoutInflater
The third method is to override Activity attachBaseContext and ContextThemeWrapper’s getSystemService method to return a custom LayoutInflater. Custom LayoutInflaters can override the setFactory2 method to add their own processing logic. This is a method I learned from ViewPump.
Little details
Here are a few details about how the AppCompatDelegate can load a view.
onCreateView
We want Factory2’s onCreateView method to call createView directly (mentioned in the section on proxies to AppCompatDelegate). In fact, that’s exactly what they did. But there’s one more thing in the code – callActivityOnCreateView is called. In AppCompatDelegateImplV14 it looks like this:
@Override
View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
// On Honeycomb+, Activity's private inflater factory will handle
// calling its onCreateView(...)
return null;
}
Copy the code
If you look at the source code for LayoutInflater, createViewFromTag tries to get a view from the Factory. If not, mPrivateFactory is used. If not, the view will be created using the view TAB. MPrivateFactory is set in the Activity.
Interestingly, the purpose of mPrivateFactory is to parse fragment tags.
Prior to API 14, LayoutInflaters did not provide a mPrivateFactory for activities to create views in a backpocket scheme. Therefore, callActivityOnCreateView provides this functionality in earlier versions. But that doesn’t matter now, AppCompat is currently only compatible with Api 14+ anyway.
Another interesting point is window.callback. Window.Callback is a Callback that allows the caller to intercept key distributions, panels, menus, and so on. It allows AppCompatActivity to process certain times such as menu keys, return keys, etc.
createView
In general, AppCompatDelegateImplV9 does two things. First, you create AppCompatViewInflater or any other subclass specified in theme. Second, create views using inflater.
AppCompatViewInflater createView uses the correct Context (which needs to be wrapped considering app:theme and Android :theme support), Create the corresponding AppCompat component based on the component name (for example, if it is a TextView, call the createTextView method to return AppCompatTextView).
supportapp:theme
Starting with Android 5.0, you can set app: Theme to a View to override the properties of a particular View and its subclasses. AppCompat replicates this behavior prior to Android 5.0 by inheriting the parent View’s context.
Before AppCompat loads the View, it grabs the parent View’s Context and tries to create a ContextThemeWrapper (Android :theme or app:theme), Ensure that the correct context is used to load the component.
In addition, AppCompat also provided TintContextWrapper to wrap Context prior to Android 5.0 if the developer explicitly stated the need to use vector graphics in the resource.
View creation and bottom pocket
With this information, the system is ready to create the View.
Iterate through the list of supported components, and for generic views such as TextView, ImageView, directly generate the corresponding AppCompat subclass. If the View is of an unknown type createView will be called using the correct Context, which returns null by default but will generally be overridden by subclasses of AppCompatViewInflater.
If the view is still null, the view’s original context is checked to see if it matches the parent view’s context. This happens when the child View’s Android :theme and parent View don’t match.
After checking Android :onClick, the view is returned.
Summary and use examples
To summarize, AppCompatActivity can step into the View creation process by setting Factory2 to LayoutInflater to provide backward compatibility (providing TINt for components, handling Android :theme, etc.). It also ensures extensibility, allowing developers to do some customization.
This technique has been used to do more interesting things than Appcompat. Probe (now deprecated) provides the OvermeasureInterceptor to measure views, and the LayoutBoundsInterceptor to highlight View boundaries.
The Calligraphy uses this technique to easily add fonts to textViews. It uses the ViewPump library, which provides some possible uses in the wiki.
Finally, Google’s Material Components for Android replaces the Button with the MaterialButton by customizing AppCompatViewInflater.