英文:
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"
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论