英文:
How to inflate Views like in Activity, so that TextView will become MaterialTextView, for example?
问题
class 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)
}
private 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) {
AppCompatViewInflater()
} else {
try {
val viewInflaterClass: Class<*> =
context.classLoader.loadClass(viewInflaterClassName)
viewInflaterClass.getDeclaredConstructor()
.newInstance() as AppCompatViewInflater
} catch (t: Throwable) {
AppCompatViewInflater()
}
}
mAppCompatViewInflater = appCompatViewInflater
}
return appCompatViewInflater.createView(parent, name!!, context, attrs, false, false, true,
VectorEnabledTintResources.shouldBeUsed())
}
}
val layoutInflater = LayoutInflater.from(context)!!
LayoutInflaterCompat.setFactory2(layoutInflater, factory)
return layoutInflater
}
}
英文:
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?
答案1
得分: 1
I understand. Here is the translated code:
从AppCompat版本1.5.1到1.6.0,_AppcompatViewInflator#createView_的修饰符从`final View createView`变为了`public final View createView`。尽管Android Studio会对`VectorEnabledTintResources.shouldBeUsed()`报错,但仍然可以成功调用。
在AppCompat版本1.6.0及更高版本中,可以如下构建Material LayoutInflater:
```kotlin
private fun getMaterialInflater(context: Context, @StyleRes theme: Int): LayoutInflater {
val inflater = ContextThemeWrapper(context, theme).run {
getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
}
val factory = object : LayoutInflater.Factory2 {
val myInflater = MaterialComponentsViewInflater()
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {
return myInflater.createView(
parent,
name,
context,
attrs,
false, // "true" to use the parent's context
false, // androidTheme: Emulate the android:theme attribute for devices before L.
true, // appTheme: Emulate the android:theme attribute for devices before L.
VectorEnabledTintResources.shouldBeUsed() /* 仅在启用时对上下文进行色彩调整 */
)
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return onCreateView(null, name, context, attrs)
}
}
LayoutInflaterCompat.setFactory2(inflater, factory)
return inflater
}
以下是如何使用我们的_MaterialInflater_来填充布局:
getMaterialInflater().inflate(R.layout.overlay_view, null)
我将上述代码放入了一个Service中,并将以下布局作为屏幕叠加层进行了填充:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableTint="?colorAccent"
android:text="我是一个素材见证者。"
android:textSize="24sp"
app:drawableStartCompat="@drawable/baseline_wifi_24" />
</FrameLayout>
这是输出:
这是我用于演示的服务代码:
class OverlayService : Service() {
// ... (略去了部分代码)
}
希望对你有所帮助!如果你需要任何其他信息,请随时告诉我。
英文:
The modifiers for AppcompatViewInflator#createView changed from final View createView to public final View createView between AppCompat versions 1.5.1 and 1.6.0. Although Android Studio complains about VectorEnabledTintResources.shouldBeUsed(), it can still be successfully called.
A Material LayoutInflater can be built as follows for AppCompat versions 1.6.0 and later.
private fun getMaterialInflater(context: Context, @StyleRes theme: Int): LayoutInflater {
val inflater = ContextThemeWrapper(context, theme).run {
getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
}
val factory = object : LayoutInflater.Factory2 {
val myInflater = MaterialComponentsViewInflater()
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {
return myInflater.createView(
parent,
name,
context,
attrs,
false, // "true" to use the parent's context
false, // androidTheme: Emulate the android:theme attribute for devices before L.
true, // appTheme: Emulate the android:theme attribute for devices before L.
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
)
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return onCreateView(null, name, context, attrs)
}
}
LayoutInflaterCompat.setFactory2(inflater, factory)
return inflater
}
Here is how we can inflate a layout using our MaterialInflater.
getMaterialInflater().inflate(R.layout.overlay_view, null)
I have placed the above code into a Service and inflated the following layout as a screen overlay:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableTint="?colorAccent"
android:text="I'm a Material witness."
android:textSize="24sp"
app:drawableStartCompat="@drawable/baseline_wifi_24" />
</FrameLayout>
Here is the output:
Here is the service code I used for the demo:
class OverlayService : Service() {
private val mWindowManager by lazy {
getSystemService(WINDOW_SERVICE) as WindowManager
}
private lateinit var mOverlayView: View
override fun onCreate() {
super.onCreate()
startForeground(1, getNotification())
mOverlayView = createOverlayView()
// Click for graceless exit.
mOverlayView.setOnClickListener {
stopSelf()
}
addOverlayViewToWindow(mOverlayView)
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int) = START_NOT_STICKY
override fun onDestroy() {
super.onDestroy()
mWindowManager.removeView(mOverlayView)
}
private fun getMaterialInflater(context: Context, @StyleRes theme: Int): LayoutInflater {
val inflater = ContextThemeWrapper(context, theme).run {
getSystemService(LAYOUT_INFLATER_SERVICE) as LayoutInflater
}
val factory = object : LayoutInflater.Factory2 {
val myInflater = MaterialComponentsViewInflater()
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {
return myInflater.createView(
parent,
name,
context,
attrs,
false, // "true" to use the parent's context
false, // androidTheme: Emulate the android:theme attribute for devices before L.
true, // appTheme: Emulate the android:theme attribute for devices before L.
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
)
}
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return onCreateView(null, name, context, attrs)
}
}
LayoutInflaterCompat.setFactory2(inflater, factory)
return inflater
}
@SuppressLint("InflateParams")
private fun createOverlayView(): View {
return getMaterialInflater(
this, // The service's context
R.style.Theme_MyMaterialTheme_Service // Theme to use for inflation
).inflate(R.layout.overlay_view, null)
}
private fun addOverlayViewToWindow(overlayView: View) {
val params = LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT,
LayoutParams.TYPE_APPLICATION_OVERLAY,
LayoutParams.FLAG_NOT_FOCUSABLE or
LayoutParams.FLAG_NOT_TOUCH_MODAL or
LayoutParams.FLAG_LAYOUT_IN_SCREEN,
PixelFormat.TRANSLUCENT
)
mWindowManager.addView(overlayView, params)
}
private fun getNotification(): Notification {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_MIN
)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
val notificationBuilder =
NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE)
return notificationBuilder.setOngoing(true)
.setContentTitle("Demo overlay app is running")
.setContentText("Displaying over other apps")
.setSmallIcon(R.drawable.baseline_wifi_24)
.setPriority(NotificationManager.IMPORTANCE_MIN)
.setCategory(Notification.CATEGORY_SERVICE)
.build()
}
private companion object {
const val NOTIFICATION_CHANNEL_ID = "demoapp.notouch"
const val CHANNEL_NAME = "Foreground Service"
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论