【Android】自定义换肤框架03之自定义LayoutInflaterFactory

AppCompatActivity是如何创建View的
  • Activity通过LayoutInflater解析出XmlLayout相关信息
  • LayoutInflater内部维护了一个InflaterFactory对象
  • InflaterFactory接口包含了一个onCreateView方法,用于创建View
  • 将解析出的Xml信息转为AttributeSet,交给InflaterFactory来createView
  • AppCompatActivity中维护了一个AppCompatDelegate对象
  • 这个对象既用于处理兼容性工作,也实现了InflaterFactory接口
  • 在Activity执行onCreate方法时,会调用installViewFactory,将delegate设置为LayoutInflater的Factory2
LayoutInflater.Factory2和LayoutInflater.Factory
  • Factory2是新版本的Factory接口,Factory是旧接口
  • 当Factory2存在时,会忽略Factory,反之则使用Factory来创建View
  • Factory2是为了兼容旧版本代码和而引入的,通过delegate和factory轻松实现了两套逻辑的切换
自定义LayoutInflaterFactory

在上一章,我们实现了自定义AssetManager和Resources,但不知道在哪里去应用它们

现在我们知道,View是通过InflaterFactory创建的

如果我们能让Factory使用自定义Resources,那么基本就实现了换肤的功能

先上代码,让大家心里有个底

kotlin 复制代码
package com.android.app

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class HomeActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        layoutInflater.factory2 = SkinnerInflaterFactory(this)
        super.onCreate(savedInstanceState)
        val root = layoutInflater.inflate(R.layout.activity_home, null)
        setContentView(root)
    }
}
kotlin 复制代码
package com.android.app

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import com.android.library.skinner.SkinnerAssetManager

typealias androidStyleableRes = androidx.appcompat.R.styleable

class SkinnerInflaterFactory(private val activity: AppCompatActivity) : LayoutInflater.Factory2 {

    override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
        val view = activity.delegate.createView(parent, name, context, attrs)
        if (view is ImageView) {
            skinImageView(view, attrs)
        }
        return view
    }

    override fun onCreateView(name: String, context: Context, attrs: AttributeSet) = null

    private fun skinImageView(view: ImageView, attrs: AttributeSet) {
        val typedArray = activity.obtainStyledAttributes(attrs, androidStyleableRes.AppCompatImageView)
        if (typedArray.hasValue(androidStyleableRes.AppCompatImageView_android_src)) {
            val srcDrawableId = typedArray.getResourceId(androidStyleableRes.AppCompatImageView_android_src, 0)
            val skinDrawable = SkinnerAssetManager.skinDrawable(srcDrawableId)
            view.setImageDrawable(skinDrawable)
        }
    }
}

代码其实非常简单,如果是自己实现的话,以下点需要注意

  • InflaterFactory一旦创建,不可再被修改,除非通过反射强制去修改
  • InflaterFactory默认是在onCreate方法里创建的,如果我们想使用自定义的,则需在onCreate之前设置
  • 由于InflaterFactory是在onCreate方法中设置的,意味着如果想中途换肤,则必须重启Activity甚至Application才会生效
  • 如果想让新的InflaterFactory立刻生效,只能通过反射去强制修改,然后再调用setContentView重新加载布局
  • InflaterFactory是从零开始创建完整的View,这意味着我们可以去做任何事情,只要不嫌麻烦
  • 比如读到name=TextView时,我们可以创建一个Button返回,完成控件替换
  • 比如读到name=TextView时,我们可以创建一个AppCompatTextView返回,完成旧控件自动升级
  • 当然,创建一个完整的View,而且是Xml中可能出现的所有View,工作量是非常庞大的
  • 我们要的只是更换皮肤,即修改部分属性对应的资源,没必要去自己去创建View
  • 我们可以调用默认的Factory去创建View,然后再修改我们想要的属性值即可
  • 上面代码中用到的activity.delegate.createView即是AppCompatActivity的默认Factory
  • 如果我们没有自定义Factory的话,activity.delegate就会成为默认的LayoutInflater.factory2
使用自定义皮肤资源

上面已经给出了自定义皮肤资源的代码

kotlin 复制代码
typealias androidStyleableRes = androidx.appcompat.R.styleable
private fun skinImageView(view: ImageView, attrs: AttributeSet) {
    val typedArray = activity.obtainStyledAttributes(attrs, androidStyleableRes.AppCompatImageView)
    if (typedArray.hasValue(androidStyleableRes.AppCompatImageView_android_src)) {
        val srcDrawableId = typedArray.getResourceId(androidStyleableRes.AppCompatImageView_android_src, 0)
        val skinDrawable = SkinnerAssetManager.skinDrawable(srcDrawableId)
        view.setImageDrawable(skinDrawable)
    }
}

在这段代码里,我们做了以下工作

  • 判断控件类型是不是我们想要修改的
  • 找到改控件对应的样式空间,即styleable.namespace
  • 找到自己想要修改的属性,即styleable.namespace_attr
  • 皮肤包中如果存在该资源,则使用皮肤包中的资源,否则使用安装包中的默认资源
  • 以上动态加载资源的过程,是通过SkinnerAssetManager去实现的
十万个为什么

如果只是一个Demo的话,到此为止已经完美实现功能了

但是在实际应用中,我们可能需要支持任意控件,任意属性的修改

这意味着,上一节的代码,可能需要上百段雷同的代码,才能满足所有的要求

并且,哪些属性需要适配换肤功能,Factory也是不知道的,需要我们想办法去指定

理想的情况是,所有资源通过Resources加载,然后根据资源名称对Resources进行Hook

遗憾的是,安卓并未支持以上机制,所以目前已有的皮肤适配方案,都一定程度上依赖手动去配置

下一章,我们将讲解,如何支持全控件全属性适配,并且能够适当简化编码

相关推荐
Winston Wood2 小时前
Android Parcelable和Serializable的区别与联系
android·序列化
清风徐来辽2 小时前
Android 项目模型配置管理
android
帅得不敢出门2 小时前
Gradle命令编译Android Studio工程项目并签名
android·ide·android studio·gradlew
problc3 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter
帅得不敢出门13 小时前
安卓设备adb执行AT指令控制电话卡
android·adb·sim卡·at指令·电话卡
我又来搬代码了15 小时前
【Android】使用productFlavors构建多个变体
android
德育处主任17 小时前
Mac和安卓手机互传文件(ADB)
android·macos
芦半山17 小时前
Android“引用们”的底层原理
android·java
迃-幵17 小时前
力扣:225 用队列实现栈
android·javascript·leetcode
大风起兮云飞扬丶17 小时前
Android——从相机/相册获取图片
android