1.通过Theme切换主题

通过在setContentView之前设置Theme实现主题切换。

在styles.xml定义一个夜间主题和白天主题:

<style name="LightTheme" parent="Theme.AppCompat.Light.DarkActionBar"><item name="colorPrimary">@color/colorPrimary</item><item name="colorPrimaryDark">@color/colorPrimaryDark</item><item name="colorAccent">@color/colorAccent</item><!--主题背景--><item name="backgroundTheme">@color/white</item>
</style><style name="BlackTheme" parent="Theme.AppCompat.Light.DarkActionBar"><item name="colorPrimary">@color/colorPrimary</item><item name="colorPrimaryDark">@color/colorPrimaryDark</item><item name="colorAccent">@color/colorAccent</item><!--主题背景--><item name="backgroundTheme">@color/dark</item>
</style>

设置主要切换主题View的背景:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="?attr/backgroundTheme"tools:context=".MainActivity"><Buttonandroid:id="@+id/btn"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="切换主题"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent" /></android.support.constraint.ConstraintLayout>

切换主题:

通过调用setTheme()

@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setTheme(R.style.BlackTheme);setContentView(R.layout.activity_main);
}finish();
Intent intent = getIntent();
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
overridePendingTransition(0, 0);

2.通过AssetManager切换主题

下载皮肤包,通过AssetManager加载皮肤包里面的资源文件,实现资源替换。

ClassLoader

Android可以通过classloader获取已安装apk或者未安装apk、dex、jar的context对象,从而通过反射去获取Class、资源文件等。

加载已安装应用的资源

//获取已安装app的context对象
Context context = ctx.getApplicationContext().createPackageContext("com.noob.resourcesapp",         Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
//获取已安装app的resources对象
Resources resources = context.getResources();
//通过resources获取classloader,反射获取R.class
Class aClass = context.getClassLoader().loadClass("com.noob.resourcesapp.R$drawable");
int resId = (int) aClass.getField("icon_collect").get(null);
imageView.setImageDrawable(resources.getDrawable(id));

加载未安装应用的资源

String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.apk";
//通过反射获取未安装apk的AssetManager
AssetManager assetManager = AssetManager.class.newInstance();
//通过反射增加资源路径
Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
method.invoke(assetManager, apkPath);
File dexDir = ctx.getDir("dex", Context.MODE_PRIVATE);
if (!dexDir.exists()) {dexDir.mkdir();
}
//获取未安装apk的Resources
Resources resources = new Resources(assetManager, ctx.getResources().getDisplayMetrics(),ctx.getResources().getConfiguration());
//获取未安装apk的ClassLoader
ClassLoader classLoader = new DexClassLoader(apkPath, dexDir.getAbsolutePath(), null, ctx.getClassLoader());
//反射获取class
Class aClass = classLoader.loadClass("com.noob.resourcesapp.R$drawable");
int id = (int) aClass.getField("icon_collect").get(null);
imageView.setImageDrawable(resources.getDrawable(id));

3.在view生成的时候去动态加载背景

思考先:一个TextView在xml简析时,后面会经历什么呢?setContentView(R.id.activity_main)后面经历了什么呢?

直接说答案:

Activity -> OnCreat的流程如下:

然后:

这里注意一点layoutInflater.getFactory(),返回的是LayoutInflater的一个内部接口Factory。默认没有人为干预的情况下,我们不设置Factory的情况下,layoutInflater.getFactory()等于null,系统会自己创建一个Factory去处理XML到View的转换。反之,如果我们设置了自己的Factory,那么系统就会走我们Factory的onCreateView,他会返回一个我们定制化的View。

Factory定义如下:

public interface Factory {/*** Hook you can supply that is called when inflating from a LayoutInflater.* You can use this to customize the tag names available in your XML* layout files.* * <p>* Note that it is good practice to prefix these custom names with your* package (i.e., com.coolcompany.apps) to avoid conflicts with system* names.* * @param name Tag name to be inflated.* @param context The context the view is being created in.* @param attrs Inflation attributes as specified in XML file.* * @return View Newly created view. Return null for the default*         behavior.*/public View onCreateView(String name, Context context, AttributeSet attrs);}

Factory

Factory是一个很强大的接口。当我们使用inflating一个XML布局时,可以使用这个类进行拦截解析到的XML中的标签属性-AttributeSet和上下文-Context,以及标签名称-name(例如:TextView)。然后我们根据这些属性可以创建对应的View,设置一些对应的属性。

比如:我读取到XML中的TextView标签,这时,我就创建一个AppCompatTextView对象,它的构造方法中就是我读取到的XML属性。然后,将构造好的View返回即可。

默认情况下,从context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)得到LayoutInflater,通过layoutInflater.getFactory()刚开始是null,然后执行LayoutInflaterCompat.setFactory(layoutInflater, this);方法。

看下这个方法:

  * Attach a custom Factory interface for creating views while using* this LayoutInflater. This must not be null, and can only be set once;* after setting, you can not change the factory.** @see LayoutInflater#setFactory(android.view.LayoutInflater.Factory)*/public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {IMPL.setFactory(inflater, factory);}

在这里我们关注下传入的LayoutInflaterFactory的实例,最终这个设置的LayoutInflaterFactory传入到哪里了呢?向下debug,进入LayoutInflater中的下面:

给mFactory = mFactory2 = factory执行了,进行mFactory和mFactory2的赋值。

到这里为止,初始化好了LayoutInflater和LayoutInflaterFactory。

好了,现在就走完了SelectThemeActivity#onCreate中的super.onCreate(savedInstanceState);下面开始走setContentView(R.layout.activity_select_theme);

setContentView(int resId)

setContentView会走到LayoutInflate的下面这里:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {final Resources res = getContext().getResources();if (DEBUG) {Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("+ Integer.toHexString(resource) + ")");}//在这里将Resource得到layout的XmlResourceParser对象final XmlResourceParser parser = res.getLayout(resource);try {return inflate(parser, root, attachToRoot);} finally {parser.close();}}

再向下就到了LayoutInflate重点:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {synchronized (mConstructorArgs) {.....//将上面给我的XmlPullParser转换为对应的View的属性AttributeSet供View的构造方法或其他方法使用final AttributeSet attrs = Xml.asAttributeSet(parser);....try {if{....} else {//默认布局会走到这里,Temp是XML文件的根布局// Temp is the root view that was found in the xmlfinal View temp = createViewFromTag(root, name, inflaterContext, attrs);...// Inflate all children under temp against its context.rInflateChildren(parser, temp, attrs, true);....//添加解析到的根View// to root. Do that now.if (root != null && attachToRoot) {root.addView(temp, params);}....}} catch (XmlPullParserException e) {....return result;}}

进入到createViewFromTag方法之中,会进入到LayoutInflate的View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)中。

这里的name传入的就是就是解析到的标签值LinearLayout。

@Overridepublic final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {// First let the Activity's Factory try and inflate the view先试着进行解析布局final View view = callActivityOnCreateView(parent, name, context, attrs);if (view != null) {return view;}// If the Factory didn't handle it, let our createView() method tryreturn createView(parent, name, context, attrs);}

很遗憾, callActivityOnCreateView返回的总是null:

@OverrideView callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {// On Honeycomb+, Activity's private inflater factory will handle calling its// onCreateView(...)return null;}

然后进入到下面的,createView(parent, name, context, attrs);中。重点来了!!!,期盼已久的看看Google源码是如何创建View的。

从XML到View的华丽转身

担心图片失效,再复制一遍代码:

public final View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs, boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {final Context originalContext = context;// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy// by using the parent's contextif (inheritContext && parent != null) {context = parent.getContext();}if (readAndroidTheme || readAppTheme) {// We then apply the theme on the context, if specifiedcontext = themifyContext(context, attrs, readAndroidTheme, readAppTheme);}if (wrapContext) {context = TintContextWrapper.wrap(context);}View view = null;// We need to 'inject' our tint aware Views in place of the standard framework versionsswitch (name) {case "TextView":view = new AppCompatTextView(context, attrs);break;case "ImageView":view = new AppCompatImageView(context, attrs);break;case "Button":view = new AppCompatButton(context, attrs);break;case "EditText":view = new AppCompatEditText(context, attrs);break;case "Spinner":view = new AppCompatSpinner(context, attrs);break;case "ImageButton":view = new AppCompatImageButton(context, attrs);break;case "CheckBox":view = new AppCompatCheckBox(context, attrs);break;case "RadioButton":view = new AppCompatRadioButton(context, attrs);break;case "CheckedTextView":view = new AppCompatCheckedTextView(context, attrs);break;case "AutoCompleteTextView":view = new AppCompatAutoCompleteTextView(context, attrs);break;case "MultiAutoCompleteTextView":view = new AppCompatMultiAutoCompleteTextView(context, attrs);break;case "RatingBar":view = new AppCompatRatingBar(context, attrs);break;case "SeekBar":view = new AppCompatSeekBar(context, attrs);break;}if (view == null && originalContext != context) {// If the original context does not equal our themed context, then we need to manually// inflate it using the name so that android:theme takes effect.view = createViewFromTag(context, name, attrs);}if (view != null) {// If we have created a view, check it's android:onClickcheckOnClickListener(view, attrs);}return view;}

可以看到,它是拿标签名称进行switch的比较,是哪一个就进入到哪一个中进行创建View。

有人会说,这里没有LinearLayout对应的switch啊。的确。最终返回null。

回到最初,由于Line769返回null,同时name值LinearLayout不包含".",进入到Line785onCreateView(parent, name, attrs)

到这里,知道这个标签是LinearLayout了,那么开始创建这个对象了。问题来了,我们知道这个对象名称了,但是它属于哪个包名?如何创建呢?

根据标签名称创建对象

我们知道,Android控件中的包名总共就那么几个:android.widget、android.webkit、android.app,既然就这么几种,干脆挨个用这些字符串进行如下拼接:
android.widget.LinearLayout、android.webkit.LinearLayout、android.app.LinearLayout、,然后挨个创建对象,一旦创建成功即说明这个标签所在的包名是对的,返回这个对象即可。

那么,从上面debug会进入到如下源码:

sClassPrefixList的定义如下:

private static final String[] sClassPrefixList = {"android.widget.","android.webkit.","android.app."};

注意:是final的

创建Android布局标签对象

继续向下,进入到真正的创建Android布局标签对象的实现。在这个方法中,才是“android.widget.”包下的,LinearLayout、RelativeLayout等等的具体实现

name="LinearLayout"
prefix="android.widget."

分析下这段代码(下面的方法中去掉了一些无用代码):

public final View createView(String name, String prefix, AttributeSet attrs)throws ClassNotFoundException, InflateException {
//step1 :sConstructorMap是<标签名称:标签对象>的map,用来缓存对象的。第一次进入时,这个map中是空的。Constructor<? extends View> constructor = sConstructorMap.get(name);if (constructor != null && !verifyClassLoader(constructor)) {constructor = null;sConstructorMap.remove(name);}Class<? extends View> clazz = null;try {
//step2:在map缓存中没有找到对应的LinearLayout为key的对象,则创建。if (constructor == null) {// Class not found in the cache, see if it's real, and try to add it//step3:【关键点,反射创建LinearLayout对象】,根据"prefix + name"值是"android.widget.LinearLayout"加载对应的字节码文件对象。clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);if (mFilter != null && clazz != null) {boolean allowed = mFilter.onLoadClass(clazz);if (!allowed) {failNotAllowed(name, prefix, attrs);}}
//step4:获取LinearLayout的Constructor对象constructor = clazz.getConstructor(mConstructorSignature);constructor.setAccessible(true);
//step5:缓存LinearLayout的Constructor对象sConstructorMap.put(name, constructor);} else {// If we have a filter, apply it to cached constructorif (mFilter != null) {// Have we seen this name before?Boolean allowedState = mFilterMap.get(name);if (allowedState == null) {// New class -- remember whether it is allowedclazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);boolean allowed = clazz != null && mFilter.onLoadClass(clazz);mFilterMap.put(name, allowed);if (!allowed) {failNotAllowed(name, prefix, attrs);}} else if (allowedState.equals(Boolean.FALSE)) {failNotAllowed(name, prefix, attrs);}}}Object[] args = mConstructorArgs;args[1] = attrs;
//step6:args的两个值分别是SelectThemeActivity,XmlBlock$Parser。到这里就调用了LinearLayout的两个参数的构造方法去实例化对象。至此,LinearLayout的实现也就是Android中的布局文件的实现全部完成。最后把创建的View给return即可。final View view = constructor.newInstance(args);if (view instanceof ViewStub) {// Use the same context when inflating ViewStub later.final ViewStub viewStub = (ViewStub) view;viewStub.setLayoutInflater(cloneInContext((Context) args[0]));}return view;} ......}

在这个方法中关键的步骤就是如何去实例化布局标签对象。这也是我们换肤的前提知识。

总结下根据标签+属性创建View的思路:

两个关键点:

  • 是否设置了Factory
  • Factory的onCreateView是否返回null

再让我们回到最初的地方:


View view;if (mFactory2 != null) {view = mFactory2.onCreateView(parent, name, context, attrs);} else if (mFactory != null) {view = mFactory.onCreateView(name, context, attrs);} else {view = null;}if (view == null && mPrivateFactory != null) {view = mPrivateFactory.onCreateView(parent, name, context, attrs);}
//请注意下面												

Android 几种换肤方式和原理分析相关推荐

  1. Android三种换肤方案原理及Demo

    方案一:使用主题文件 定义换肤资源 在values/下新建一个xml文件,比如theme_attrs.xml,然后定义换肤的资源类型 <?xml version="1.0" ...

  2. Android应用内换肤

    换肤简介 换肤本质上是对资源的一种替换包括.字体.颜色.背景.图片.大小等等.比如View的修改背景颜色setBackgroundColor,TextView的setTextSize修改字体等等. 换 ...

  3. Android App节日换肤

    Android App节日换肤 Android App节日换肤 1原理 2使用方式 1在XML中给需要换肤的控件添加tag属性 2在Activity中使用 3还有疑问吧 3示例图 比如支付宝,饿了么, ...

  4. Android应用程序换肤实现系列(一)

    转载请标明出处:http://blog.csdn.net/EdisonChang/article/details/50021467 国内已经有很多android应用软件支持个性化皮肤定制功能,比如QQ ...

  5. Android插件化换肤

    Android插件化换肤 前言(废话) 今年是大年三十,今年怎么说呢,总体还是让自己感觉到比较满意的,但是有些时候还是感觉自己的自觉性不够.先贤曾经说过,君子慎独,愿明年的我能够铭记于心. 我这辈子最 ...

  6. Android美女一键换肤

    今天看到有同事在群里聊到仿QQQ 皮肤切换主题,然后我就试着写一个简单的Demo希望大家能够支持下! 像以前扣扣换肤 废话不多说上效果 思路如下 通过应用程序内置资源实现换肤,典型的应用为QQ空间中换 ...

  7. 几种换肤软件使用问题

    几种换肤软件的使用和使用中的BUG                                                                                    ...

  8. Android模拟器的换肤和Android学习资料下载

          现如今,说到Android,不知者,可以说是寥寥无几,就连邻家小妹,也在玩Android.Android的火爆,足以看到移动市场可见一斑啊.移动市场的巨大空间,巨大商机,很多人都在蠢蠢欲动 ...

  9. android 7种网络连接方式--IT蓝豹

    2019独角兽企业重金招聘Python工程师标准>>> 本项目由作者 王永飞 精心为初学者准备的学习项目. android 几种网络连接方式,本项目适合初学者学习网络知识. 项目中用 ...

最新文章

  1. 禁用编译优化_Tomcat8史上最全优化实践
  2. 微软华人团队刷新COCO记录!全新目标检测机制达到SOTA|CVPR 2021
  3. latex参考文献顺序不对_latex模板中,引用多篇参考文献,连续引用压缩问题
  4. saslauthd mysql_启用MemCached的SASL认证
  5. excel通过js导入到页面_基于Excel和Java自动化工作流程:发票生成器示例
  6. 使用SoapHeader对WebService进行身份验证
  7. Javascript 正则表达式对象
  8. delphi7 安装delphi 5 delphi 6控件
  9. 批标准化(batch normalization)与层标准化(layer normalization)比较
  10. PowerDesigner将PDM导出生成WORD文档(rtf文档)
  11. python数学符号读法大全_数学符号读法大全
  12. java获取身份证上的出生日期
  13. 小程序毕业设计 基于微信会议室预约小程序毕业设计开题报告功能参考
  14. IG赢了,让我们先理直气壮的喊出那句 我们是冠军!
  15. 鸽巢原理(初识)(纯算法)
  16. 工具说明书 - 使用网页生成条码
  17. ZIP压缩包密码加密、解密
  18. 【Python计量】Logit模型
  19. Python爬虫403错误的解决方案
  20. shell编程范例之字符串操作[转]

热门文章

  1. spring单元测试无法注入bean_2019年,最新的Spring 面试108题 “ 系列 ”,附带答案.........
  2. class不生效 weblogic_weblogic下更改jsp不生效的解决办法
  3. [BZOJ3262]陌上花开
  4. linux免交互登陆远程主机并执行命令(密钥对和Expect)
  5. 迁移到云:渐进但不可逆转
  6. CAS (3) —— Mac下配置CAS客户端经代理访问Tomcat CAS
  7. PC问题-VMware Workstation出现“文件锁定失败”
  8. 【技术随笔】学习C语言之前你要知道的事
  9. CentOS上如何把Web服务器从Apache换到nginx
  10. MVC5+EF6 入门完整教程七