Background
I work on an app that doesn't always have an Activity (or AppCompatActivity to be precise). Sometimes it has a floating UI instead (using SAW permission), so it has only ApplicationContext.
The problem
Some of the UI is inflated there, and it could be nice to use the latest material libraries.
One example is MaterialTextView, which on normal situations it's inflated to replace TextView in the layout file:
<FrameLayout 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:layout_width="match_parent"
android:layout_height="match_parent" tools:context=".MainActivity">
<TextView
android:id="@+id/textView" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_gravity="center"
android:text="Hello World!" app:drawableStartCompat="@drawable/customringtone" />
</FrameLayout>
For this example alone, I will demonstrate it in an Activity. This would let the TextView to show its drawable:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = LayoutInflater.from(this)
val binding = ActivityMainBinding.inflate(inflater, null, false)
setContentView(binding.root)
}
}
But, to show you how it works outside, use it a different inflater, which doesn't use the Activity:
val inflater =
LayoutInflater.from(android.view.ContextThemeWrapper(applicationContext, R.style.AppTheme))
And the theme:
<style name="AppTheme" parent="@style/Theme.Material3.Light.NoActionBar">
</style>
In this case, the drawable of the TextView doesn't show up, and indeed if I check it in code (using compoundDrawablesRelative and compoundDrawables on it), I can see it has no drawables.
So, currently the workaround I have is to use MaterialTextView on my own, or use the older attributes and let the IDE ignore the suggestion to use app:drawableStartCompat.
EDIT: later when talking with Google, they wrote me:
This is already available via the AppCompatViewInflater API.
You install the AppCompatViewInflater API by creating an implementation of the LayoutInflater.Factory2 interface, overriding its onCreateView APIs to call AppCompatViewInflater's createView API. You can then install the factory onto your LayoutInflater via LayoutInflaterCompat.setFactory2(layoutInflater, yourFactory). See the source code for AppCompatDelegateImpl for an example.
Thing is, this solution has multiple issues:
- A lot of private functions
- A lot of code to copy
- very fragile as it depends on a library's implementation and relation between quite inner classes, but it won't work because it fails to be built.
Still, wanted to try (wrote about it here):
import android.content.Context
import android.content.res.TypedArray
import android.os.Build
import android.util.*
import android.view.*
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatViewInflater
import androidx.appcompat.widget.VectorEnabledTintResources
import androidx.core.view.LayoutInflaterCompat
object MaterialInflater {
fun getMaterialInflater(context: Context): LayoutInflater {
val factory = object : LayoutInflater.Factory2 {
private var mAppCompatViewInflater: AppCompatViewInflater? = null
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
return createView(parent, name, context, attrs);
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return onCreateView(null, name, context, attrs);
}
fun createView(parent: View?, name: String?, context: Context,
attrs: AttributeSet): View? {
var appCompatViewInflater = mAppCompatViewInflater
if (appCompatViewInflater == null) {
val a: TypedArray =
context.obtainStyledAttributes(androidx.appcompat.R.styleable.AppCompatTheme)
val viewInflaterClassName =
a.getString(androidx.appcompat.R.styleable.AppCompatTheme_viewInflaterClass)
a.recycle()
appCompatViewInflater = if (viewInflaterClassName == null) {
// Set to null (the default in all AppCompat themes). Create the base inflater
// (no reflection)
AppCompatViewInflater()
} else {
try {
val viewInflaterClass: Class<*> =
context.classLoader.loadClass(viewInflaterClassName)
viewInflaterClass.getDeclaredConstructor()
.newInstance() as AppCompatViewInflater
} catch (t: Throwable) {
// Log.i(TAG, "Failed to instantiate custom view inflater "
// + viewInflaterClassName + ". Falling back to default.", t)
AppCompatViewInflater()
}
}
mAppCompatViewInflater = appCompatViewInflater
}
return appCompatViewInflater.createView(parent, name!!, context, attrs, false,
false, /* 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 */
)
}
}
val layoutInflater = LayoutInflater.from(context)!!
LayoutInflaterCompat.setFactory2(layoutInflater, factory)
return layoutInflater
}
}
It shows an error due to using 2 private functions:
- appCompatViewInflater.createView
- VectorEnabledTintResources.shouldBeUsed()
The error is:
e: file:///C:/Users/User/Desktop/MyApplication/app/src/main/java/com/lb/myapplication/Foo.kt:64:46 Cannot access 'createView': it is package-private in 'AppCompatViewInflater'
And that's before I even try to use this new code...
The questions
How can I make it work like on Activity, so that the inflater will replace all Views to the material version of them?
The modifiers for AppcompatViewInflator#createView changed from
final View createViewtopublic final View createViewbetween AppCompat versions 1.5.1 and 1.6.0. Although Android Studio complains aboutVectorEnabledTintResources.shouldBeUsed(), it can still be successfully called.A Material LayoutInflater can be built as follows for AppCompat versions 1.6.0 and later.
Here is how we can inflate a layout using our MaterialInflater.
I have placed the above code into a Service and inflated the following layout as a screen overlay:
Here is the output:
Here is the service code I used for the demo: