赞
踩
通过在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">
-
- <Button
- android: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);
下载皮肤包,通过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));
思考先:一个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 xml
- final 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。
- @Override
- public 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 try
- return createView(parent, name, context, attrs);
- }
很遗憾, callActivityOnCreateView返回的总是null:
- @Override
- View 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的。
担心图片失效,再复制一遍代码:
- 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 context
- if (inheritContext && parent != null) {
- context = parent.getContext();
- }
- if (readAndroidTheme || readAppTheme) {
- // We then apply the theme on the context, if specified
- context = 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 versions
- switch (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:onClick
- checkOnClickListener(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 constructor
- if (mFilter != null) {
- // Have we seen this name before?
- Boolean allowedState = mFilterMap.get(name);
- if (allowedState == null) {
- // New class -- remember whether it is allowed
- clazz = 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的思路:
两个关键点:
再让我们回到最初的地方:
-
- 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);
- }
- //请注意下面声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/86855推荐阅读
相关标签
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。