如何像在Activity中一样扩展视图,以便TextView会变成MaterialTextView,例如?

huangapple go评论53阅读模式
英文:

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:

&lt;FrameLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot; android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot; tools:context=&quot;.MainActivity&quot;&gt;

    &lt;TextView
        android:id=&quot;@+id/textView&quot; android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot; android:layout_gravity=&quot;center&quot;
        android:text=&quot;Hello World!&quot; app:drawableStartCompat=&quot;@drawable/customringtone&quot; /&gt;

&lt;/FrameLayout&gt;

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:

&lt;style name=&quot;AppTheme&quot; parent=&quot;@style/Theme.Material3.Light.NoActionBar&quot;&gt;
&lt;/style&gt;

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:

  1. A lot of private functions
  2. A lot of code to copy
  3. 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&lt;*&gt; =
                                context.classLoader.loadClass(viewInflaterClassName)
                            viewInflaterClass.getDeclaredConstructor()
                                .newInstance() as AppCompatViewInflater
                        } catch (t: Throwable) {
                            //                            Log.i(TAG, &quot;Failed to instantiate custom view inflater &quot;
                            //                                    + viewInflaterClassName + &quot;. Falling back to default.&quot;, 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 &#39;createView&#39;: it is package-private in &#39;AppCompatViewInflater&#39;

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, // &quot;true&quot; to use the parent&#39;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>

这是输出:

如何像在Activity中一样扩展视图,以便TextView会变成MaterialTextView,例如?

这是我用于演示的服务代码:

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, // &quot;true&quot; to use the parent&#39;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:

&lt;FrameLayout 
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;&gt;

    &lt;TextView
        android:id=&quot;@+id/textView&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:layout_gravity=&quot;center&quot;
        android:drawableTint=&quot;?colorAccent&quot;
        android:text=&quot;I&#39;m a Material witness.&quot;
        android:textSize=&quot;24sp&quot;
        app:drawableStartCompat=&quot;@drawable/baseline_wifi_24&quot; /&gt;

&lt;/FrameLayout&gt;

Here is the output:

如何像在Activity中一样扩展视图,以便TextView会变成MaterialTextView,例如?

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, // &quot;true&quot; to use the parent&#39;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(&quot;InflateParams&quot;)
    private fun createOverlayView(): View {
        return getMaterialInflater(
            this, // The service&#39;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(&quot;Demo overlay app is running&quot;)
            .setContentText(&quot;Displaying over other apps&quot;)
            .setSmallIcon(R.drawable.baseline_wifi_24)
            .setPriority(NotificationManager.IMPORTANCE_MIN)
            .setCategory(Notification.CATEGORY_SERVICE)
            .build()
    }

    private companion object {
        const val NOTIFICATION_CHANNEL_ID = &quot;demoapp.notouch&quot;
        const val CHANNEL_NAME = &quot;Foreground Service&quot;
    }
}

huangapple
  • 本文由 发表于 2023年5月22日 15:33:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/76303909.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定