【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

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

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

相关推荐
worker..31 分钟前
[MRCTF2020]Ezpop1
android
坐望云起1 小时前
如何在 Android 设备上更改您的位置?
android·gps·定位·fake location
.try-1 小时前
js与ios、安卓原生方法互调。
android·开发语言·javascript
姜君竹2 小时前
安卓碎片Fragment
android·java·开发语言·学习·ui
程序员不想YY啊4 小时前
【保姆级讲解C语言中的运算符的优先级!】
android·c语言·开发语言
Geeker554 小时前
恢复已删除文件工具之11 个最佳恢复文件工具
android·大数据·数据库·学习·pdf·电脑·笔记本电脑
叫我龙翔5 小时前
【C++】C++11的新特性 — function 包装器 , bind包装器
android·java·数据结构·c++·算法·学习方法
命运之手6 小时前
Android Irregular View
android·irregular·shape clip
工程师老罗6 小时前
Android笔试面试题AI答之Activity(9)
android
汪公子4927 小时前
安装glibc+mysql的权限问题
android