【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

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

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

相关推荐
大白要努力!40 分钟前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟2 小时前
Android音频采集
android·音视频
小白也想学C3 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程3 小时前
初级数据结构——树
android·java·数据结构
闲暇部落5 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX7 小时前
Android 分区相关介绍
android
大白要努力!8 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee8 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood8 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-11 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记