ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Android 无缝换肤深入了解与使用,Android开发面试书籍

2022-01-03 11:33:27  阅读:190  来源: 互联网

标签:换肤 null name attrs context Android view 无缝 View


https://github.com/xujiaji/ThemeSkinning

通过皮肤apk的全路径,可知道其包名(需要用包名来获取它的资源id)

  • skinPkgPath是apk的全路径,通过mInfo.packageName就可以得到包名
  • 代码位置:SkinManager.java

PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;

通过反射添加路径可以创建皮肤apk的AssetManager对象

  • skinPkgPath是apk的全路径,添加路径的方法是AssetManager里一个隐藏的方法通过反射可以设置。
  • 此时还可以用assetManager来访问apk里assets目录的资源。
  • 想想如果更换的资源是放在assets目录下的,那么我们可以在这里动动手脚。

AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod(“addAssetPath”, String.class);
addAssetPath.invoke(assetManager, skinPkgPath);

创建皮肤apk的资源对象

  • 获取当前的app的Resources,主要是为了创建apk的Resources

Resources superRes = context.getResources();
Resources skinResource = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());

当要通过资源id获取颜色的时候

  1. 先获取内置的颜色int originColor = ContextCompat.getColor(context, resId);
  2. 如果没有外置皮肤apk资源或就用默认资源的情况下直接返回内置颜色
  3. 通过 context.getResources().getResourceEntryName(resId);获取资源id获取它的名字
  4. 通过mResources.getIdentifier(resName, "color", skinPackageName)得到皮肤apk中该资源id。(resName:就是资源名字;skinPackegeName就是皮肤apk的包名)
  5. 如果没有获取到皮肤apk中资源id(也就是等于0)返回原来的颜色,否则返回mResources.getColor(trueResId)

通过getIdentifier方法可以通过名字来获取id,比如将第二个参数修改为layoutmipmapdrawablestring就是通过资源名字获取对应layout目录mipmap目录drawable目录string文件里的资源id

public int getColor(int resId) {
int originColor = ContextCompat.getColor(context, resId);
if (mResources == null || isDefaultSkin) {
return originColor;
}

String resName = context.getResources().getResourceEntryName(resId);

int trueResId = mResources.getIdentifier(resName, “color”, skinPackageName);
int trueColor;
if (trueResId == 0) {
trueColor = originColor;
} else {
trueColor = mResources.getColor(trueResId);
}
return trueColor;
}

当要通过资源id获取图片的时候

  1. 和上面获取颜色是差不多的
  2. 只是在图片在drawable目录还是mipmap目录进行了判断

public Drawable getDrawable(int resId) {
Drawable originDrawable = ContextCompat.getDrawable(context, resId);
if (mResources == null || isDefaultSkin) {
return originDrawable;
}
String resName = context.getResources().getResourceEntryName(resId);
int trueResId = mResources.getIdentifier(resName, “drawable”, skinPackageName);
Drawable trueDrawable;
if (trueResId == 0) {
trueResId = mResources.getIdentifier(resName, “mipmap”, skinPackageName);
}
if (trueResId == 0) {
trueDrawable = originDrawable;
} else {
if (android.os.Build.VERSION.SDK_INT < 22) {
trueDrawable = mResources.getDrawable(trueResId);
} else {
trueDrawable = mResources.getDrawable(trueResId, null);
}
}
return trueDrawable;
}

对所有view进行拦截处理

  • 自己实现LayoutInflater.Factory2接口来替换系统默认的

那么如何替换呢?

@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 fi

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整资料开源分享

nal 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

标签:换肤,null,name,attrs,context,Android,view,无缝,View
来源: https://blog.csdn.net/m0_65146275/article/details/122285349

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有