Android 无缝换肤深入了解与使用,跪了

@Override
protected void onCreate(Bundle savedInstanceState) {
mSkinInflaterFactory = new SkinInflaterFactory(this);//自定义的Factory
LayoutInflaterCompat.setFactory2(getLayoutInflater(), mSkinInflaterFactory);
super.onCreate(savedInstanceState);
}

我们使用的Activity一般是AppCompatActivity在里面的onCreate方法中也有对其的设置和初始化,但是setFactory方法只能被调用一次,导致默认的一些初始化操作没有被调用,这么操作?

  • 这是实现了LayoutInflater.Factory2接口的类,看onCreateView方法中。在进行其他操作前调用delegate.createView(parent, name, context, attrs)处理系统的那一套逻辑。
  • attrs.getAttributeBooleanValue获取当前view是否是可换肤的,第一个参数是xml名字空间,第二个参数是属性名,第三个参数是默认值。这里相当于是attrs.getAttributeBooleanValue("http://schemas.android.com/android/skin", "enable", false)
  • 代码位置:SkinInflaterFactory.java

public class SkinInflaterFactory implements LayoutInflater.Factory2 {

private AppCompatActivity mAppCompatActivity;

public SkinInflaterFactory(AppCompatActivity appCompatActivity) {
this.mAppCompatActivity = appCompatActivity;
}
@Override
public View onCreateView(String s, Context context, AttributeSet attributeSet) {
return null;
}

@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);//是否是可换肤的view
AppCompatDelegate delegate = mAppCompatActivity.getDelegate();
View view = delegate.createView(parent, name, context, attrs);//处理系统逻辑
if (view instanceof TextView && SkinConfig.isCanChangeFont()) {
TextViewRepository.add(mAppCompatActivity, (TextView) view);
}

if (isSkinEnable || SkinConfig.isGlobalSkinApply()) {
if (view == null) {
view = ViewProducer.createViewFromTag(context, name, attrs);
}
if (view == null) {
return null;
}
parseSkinAttr(context, attrs, view);
}
return view;
}
}

当内部的初始化操作完成后,如果判断没有创建好view,则需要我们自己去创建view

  • 看上一步是通过ViewProducer.createViewFromTag(context, name, attrs)来创建
  • 那么直接来看一下这个类ViewProducer,原理功能请看代码注释
  • 在AppCompatViewInflater中你可以看到相同的代码
  • 代码位置:ViewProducer.java

class ViewProducer {
//该处定义的是view构造方法的参数,也就是View两个参数的构造方法:public View(Context context, AttributeSet attrs)
private static final Object[] mConstructorArgs = new Object[2];
//存放反射得到的构造器
private static final Map<String, Constructor<? extends View>> sConstructorMap = new ArrayMap<>(); //这是View两个参数的构造器所对应的两个参数 private static final Class<?>[] sConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
//如果是系统的View或ViewGroup在xml中并不是全路径的,通过反射来实例化是需要全路径的,这里列出来它们可能出现的位置
private static final String[] sClassPrefixList = {
“android.widget.”,
“android.view.”,
“android.webkit.”
};

static View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals(“view”)) {//如果是view标签,则获取里面的class属性(该View的全名)
name = attrs.getAttributeValue(null, “class”);
}

try {
//需要传入构造器的两个参数的值
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;

if (-1 == name.indexOf(‘.’)) {//如果不包含小点,则是内部View
for (int i = 0; i < sClassPrefixList.length; i++) {//由于不知道View具体在哪个路径,所以通过循环所有路径,直到能实例化或结束
final View view = createView(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {//否则就是自定义View
return createView(context, name, null);
}
} catch (Exception e) {
//如果抛出异常,则返回null,让LayoutInflater自己去实例化
return null;
} finally {
// 清空当前数据,避免和下次数据混在一起
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}

private static View createView(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
//先从缓存中获取当前类的构造器
Constructor<? extends View> constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
// 如果缓存中没有创建过,则尝试去创建这个构造器。通过类加载器加载这个类,如果是系统内部View由于不是全路径的,则前面加上
Class<? extends View> clazz = context.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
//获取构造器
constructor = clazz.getConstructor(sConstructorSignature);
//将构造器放入缓存
sConstructorMap.put(name, constructor);
}
//设置为无障碍(设置后即使是私有方法和成员变量都可访问和修改,除了final修饰的)
constructor.setAccessible(true);
//实例化
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
}
}
}

  • 当然还有另外的方式来创建,就是直接用LayoutInflater内部的那一套
  • view = ViewProducer.createViewFromTag(context, name, attrs);删除,换成下方代码:
  • 代码位置:SkinInflaterFactory.java

LayoutInflater inflater = mAppCompatActivity.getLayoutInflater();
if (-1 == name.indexOf(‘.’))//如果为系统内部的View则,通过循环这几个地方来实例化View,道理跟上面ViewProducer里面一样
{
for (String prefix : sClassPrefixList)
{
try
{
view = inflater.createView(name, prefix, attrs);
} catch (ClassNotFoundException e)
{
e.printStackTrace();
}
if (view != null) break;
}
} else
{
try
{
view = inflater.createView(name, null, attrs);
} catch (ClassNotFoundException e)
{
e.printStackTrace();
}
}

  • sClassPrefixList的定义

private static final String[] sClassPrefixList = {
“android.widget.”,
“android.view.”,
“android.webkit.”
};

最后是最终的拦截获取需要换肤的View的部分,也就是上面SkinInflaterFactory类的onCreateView最后调用的parseSkinAttr方法

  • 定义类一个成员来保存所有需要换肤的View, SkinItem里面的逻辑就是定义了设置换肤的方法。如:View的setBackgroundColor或setColor等设置换肤就是靠它。

private Map<View, SkinItem> mSkinItemMap = new HashMap<>();

  • SkinAttr: 需要换肤处理的xml属性,如何定义请参照官方文档:https://github.com/burgessjp/ThemeSkinning

private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
//保存需要换肤处理的xml属性
List viewAttrs = new ArrayList<>();
//变量该view的所有属性
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attrName = attrs.getAttributeName(i);//获取属性名
String attrValue = attrs.getAttributeValue(i);//获取属性值
//如果属性是style,例如xml中设置:style=“@style/test_style”
if (“style”.equals(attrName)) {
//可换肤的属性
int[] skinAttrs = new int[]{android.R.attr.textColor, android.R.attr.background};
//经常在自定义View时,构造方法中获取属性值的时候使用到。
//这里通过传入skinAttrs,TypeArray中将会包含这两个属性和值,如果style里没有那就没有 - -
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, skinAttrs, 0, 0);
//获取属性对应资源的id,第一个参数这里对应下标的就是上面skinAttrs数组里定义的下标,第二个参数是没有获取到的默认值
int textColorId = a.getResourceId(0, -1);
int backgroundId = a.getResourceId(1, -1);
if (textColorId != -1) {//如果有颜色属性
//
//以上边的参照来看
//entryName就是colorAccent
String entryName = context.getResources().getResourceEntryName(textColorId);
//typeName就是color
String typeName = context.getResources().getResourceTypeName(textColorId);
//创建一换肤属性实力类来保存这些信息
SkinAttr skinAttr = AttrFactory.get(“textColor”, textColorId, entryName, typeName);
if (skinAttr != null) {
viewAttrs.add(skinAttr);
}
}
if (backgroundId != -1) {//如果有背景属性
String entryName = context.getResources().getResourceEntryName(backgroundId);
String typeName = context.getResources().getResourceTypeName(backgroundId);
SkinAttr skinAttr = AttrFactory.get(“background”, backgroundId, entryName, typeName);
if (skinAttr != null) {
viewAttrs.add(skinAttr);
}

}
a.recycle();
continue;
}
//判断是否是支持的属性,并且值是引用的,如:@color/red
if (AttrFactory.isSupportedAttr(attrName) && attrValue.startsWith(“@”)) {
try {
//去掉属性值前面的“@”则为id
int id = Integer.parseInt(attrValue.substring(1));
if (id == 0) {
continue;
}
//资源名字,如:text_color_selector

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

《960全网最全Android开发笔记》

《379页Android开发面试宝典》

《507页Android开发相关源码解析》

因为文件太多,全部展示会影响篇幅,暂时就先列举这些部分截图

tl9-1711624412596)]

《960全网最全Android开发笔记》

[外链图片转存中…(img-siF3vgvY-1711624412597)]

《379页Android开发面试宝典》

[外链图片转存中…(img-Ai5Lj34g-1711624412597)]

《507页Android开发相关源码解析》

[外链图片转存中…(img-n7ugBjfF-1711624412597)]

因为文件太多,全部展示会影响篇幅,暂时就先列举这些部分截图

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

  • 28
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值