@color/colorPrimary
当前位置:   article > 正文

Android 几种换肤方式和原理分析_android 换肤原理

android 换肤原理

1.通过Theme切换主题

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

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

  1. <style name="LightTheme" parent="Theme.AppCompat.Light.DarkActionBar">
  2. <item name="colorPrimary">@color/colorPrimary</item>
  3. <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
  4. <item name="colorAccent">@color/colorAccent</item>
  5. <!--主题背景-->
  6. <item name="backgroundTheme">@color/white</item>
  7. </style>
  8. <style name="BlackTheme" parent="Theme.AppCompat.Light.DarkActionBar">
  9. <item name="colorPrimary">@color/colorPrimary</item>
  10. <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
  11. <item name="colorAccent">@color/colorAccent</item>
  12. <!--主题背景-->
  13. <item name="backgroundTheme">@color/dark</item>
  14. </style>

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

  1. <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:app="http://schemas.android.com/apk/res-auto"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent"
  6. android:background="?attr/backgroundTheme"
  7. tools:context=".MainActivity">
  8. <Button
  9. android:id="@+id/btn"
  10. android:layout_width="wrap_content"
  11. android:layout_height="wrap_content"
  12. android:text="切换主题"
  13. app:layout_constraintBottom_toBottomOf="parent"
  14. app:layout_constraintLeft_toLeftOf="parent"
  15. app:layout_constraintRight_toRightOf="parent"
  16. app:layout_constraintTop_toTopOf="parent" />
  17. </android.support.constraint.ConstraintLayout>

切换主题:

通过调用setTheme()

  1. @Override
  2. protected void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. setTheme(R.style.BlackTheme);
  5. setContentView(R.layout.activity_main);
  6. }
  7. finish();
  8. Intent intent = getIntent();
  9. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
  10. startActivity(intent);
  11. overridePendingTransition(0, 0);

2.通过AssetManager切换主题

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

ClassLoader

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

加载已安装应用的资源

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

加载未安装应用的资源

  1. String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.apk";
  2. //通过反射获取未安装apk的AssetManager
  3. AssetManager assetManager = AssetManager.class.newInstance();
  4. //通过反射增加资源路径
  5. Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
  6. method.invoke(assetManager, apkPath);
  7. File dexDir = ctx.getDir("dex", Context.MODE_PRIVATE);
  8. if (!dexDir.exists()) {
  9. dexDir.mkdir();
  10. }
  11. //获取未安装apk的Resources
  12. Resources resources = new Resources(assetManager, ctx.getResources().getDisplayMetrics(),
  13. ctx.getResources().getConfiguration());
  14. //获取未安装apk的ClassLoader
  15. ClassLoader classLoader = new DexClassLoader(apkPath, dexDir.getAbsolutePath(), null, ctx.getClassLoader());
  16. //反射获取class
  17. Class aClass = classLoader.loadClass("com.noob.resourcesapp.R$drawable");
  18. int id = (int) aClass.getField("icon_collect").get(null);
  19. 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定义如下:

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

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);方法。

看下这个方法:

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

在这里我们关注下传入的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的下面这里:

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

再向下就到了LayoutInflate重点:

  1. public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
  2. synchronized (mConstructorArgs) {
  3. .....
  4. //将上面给我的XmlPullParser转换为对应的View的属性AttributeSet供View的构造方法或其他方法使用
  5. final AttributeSet attrs = Xml.asAttributeSet(parser);
  6. ....
  7. try {
  8. if{
  9. ....
  10. } else {
  11. //默认布局会走到这里,Temp是XML文件的根布局
  12. // Temp is the root view that was found in the xml
  13. final View temp = createViewFromTag(root, name, inflaterContext, attrs);
  14. ...
  15. // Inflate all children under temp against its context.
  16. rInflateChildren(parser, temp, attrs, true);
  17. ....
  18. //添加解析到的根View
  19. // to root. Do that now.
  20. if (root != null && attachToRoot) {
  21. root.addView(temp, params);
  22. }
  23. ....
  24. }
  25. } catch (XmlPullParserException e) {
  26. ....
  27. return result;
  28. }
  29. }

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

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

  1. @Override
  2. public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
  3. // First let the Activity's Factory try and inflate the view
  4. 先试着进行解析布局
  5. final View view = callActivityOnCreateView(parent, name, context, attrs);
  6. if (view != null) {
  7. return view;
  8. }
  9. // If the Factory didn't handle it, let our createView() method try
  10. return createView(parent, name, context, attrs);
  11. }

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

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

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

从XML到View的华丽转身

根据标签+属性创建对象

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

  1. public final View createView(View parent, final String name, @NonNull Context context,
  2. @NonNull AttributeSet attrs, boolean inheritContext,
  3. boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
  4. final Context originalContext = context;
  5. // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
  6. // by using the parent's context
  7. if (inheritContext && parent != null) {
  8. context = parent.getContext();
  9. }
  10. if (readAndroidTheme || readAppTheme) {
  11. // We then apply the theme on the context, if specified
  12. context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
  13. }
  14. if (wrapContext) {
  15. context = TintContextWrapper.wrap(context);
  16. }
  17. View view = null;
  18. // We need to 'inject' our tint aware Views in place of the standard framework versions
  19. switch (name) {
  20. case "TextView":
  21. view = new AppCompatTextView(context, attrs);
  22. break;
  23. case "ImageView":
  24. view = new AppCompatImageView(context, attrs);
  25. break;
  26. case "Button":
  27. view = new AppCompatButton(context, attrs);
  28. break;
  29. case "EditText":
  30. view = new AppCompatEditText(context, attrs);
  31. break;
  32. case "Spinner":
  33. view = new AppCompatSpinner(context, attrs);
  34. break;
  35. case "ImageButton":
  36. view = new AppCompatImageButton(context, attrs);
  37. break;
  38. case "CheckBox":
  39. view = new AppCompatCheckBox(context, attrs);
  40. break;
  41. case "RadioButton":
  42. view = new AppCompatRadioButton(context, attrs);
  43. break;
  44. case "CheckedTextView":
  45. view = new AppCompatCheckedTextView(context, attrs);
  46. break;
  47. case "AutoCompleteTextView":
  48. view = new AppCompatAutoCompleteTextView(context, attrs);
  49. break;
  50. case "MultiAutoCompleteTextView":
  51. view = new AppCompatMultiAutoCompleteTextView(context, attrs);
  52. break;
  53. case "RatingBar":
  54. view = new AppCompatRatingBar(context, attrs);
  55. break;
  56. case "SeekBar":
  57. view = new AppCompatSeekBar(context, attrs);
  58. break;
  59. }
  60. if (view == null && originalContext != context) {
  61. // If the original context does not equal our themed context, then we need to manually
  62. // inflate it using the name so that android:theme takes effect.
  63. view = createViewFromTag(context, name, attrs);
  64. }
  65. if (view != null) {
  66. // If we have created a view, check it's android:onClick
  67. checkOnClickListener(view, attrs);
  68. }
  69. return view;
  70. }

可以看到,它是拿标签名称进行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的定义如下:

  1. private static final String[] sClassPrefixList = {
  2. "android.widget.",
  3. "android.webkit.",
  4. "android.app."
  5. };

注意:是final的

创建Android布局标签对象

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

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

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

  1. public final View createView(String name, String prefix, AttributeSet attrs)
  2. throws ClassNotFoundException, InflateException {
  3. //step1 :sConstructorMap是<标签名称:标签对象>的map,用来缓存对象的。第一次进入时,这个map中是空的。
  4. Constructor<? extends View> constructor = sConstructorMap.get(name);
  5. if (constructor != null && !verifyClassLoader(constructor)) {
  6. constructor = null;
  7. sConstructorMap.remove(name);
  8. }
  9. Class<? extends View> clazz = null;
  10. try {
  11. //step2:在map缓存中没有找到对应的LinearLayout为key的对象,则创建。
  12. if (constructor == null) {
  13. // Class not found in the cache, see if it's real, and try to add it
  14. //step3:【关键点,反射创建LinearLayout对象】,根据"prefix + name"值是"android.widget.LinearLayout"加载对应的字节码文件对象。
  15. clazz = mContext.getClassLoader().loadClass(
  16. prefix != null ? (prefix + name) : name).asSubclass(View.class);
  17. if (mFilter != null && clazz != null) {
  18. boolean allowed = mFilter.onLoadClass(clazz);
  19. if (!allowed) {
  20. failNotAllowed(name, prefix, attrs);
  21. }
  22. }
  23. //step4:获取LinearLayout的Constructor对象
  24. constructor = clazz.getConstructor(mConstructorSignature);
  25. constructor.setAccessible(true);
  26. //step5:缓存LinearLayout的Constructor对象
  27. sConstructorMap.put(name, constructor);
  28. } else {
  29. // If we have a filter, apply it to cached constructor
  30. if (mFilter != null) {
  31. // Have we seen this name before?
  32. Boolean allowedState = mFilterMap.get(name);
  33. if (allowedState == null) {
  34. // New class -- remember whether it is allowed
  35. clazz = mContext.getClassLoader().loadClass(
  36. prefix != null ? (prefix + name) : name).asSubclass(View.class);
  37. boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
  38. mFilterMap.put(name, allowed);
  39. if (!allowed) {
  40. failNotAllowed(name, prefix, attrs);
  41. }
  42. } else if (allowedState.equals(Boolean.FALSE)) {
  43. failNotAllowed(name, prefix, attrs);
  44. }
  45. }
  46. }
  47. Object[] args = mConstructorArgs;
  48. args[1] = attrs;
  49. //step6:args的两个值分别是SelectThemeActivity,XmlBlock$Parser。到这里就调用了LinearLayout的两个参数的构造方法去实例化对象。至此,LinearLayout的实现也就是Android中的布局文件的实现全部完成。最后把创建的View给return即可。
  50. final View view = constructor.newInstance(args);
  51. if (view instanceof ViewStub) {
  52. // Use the same context when inflating ViewStub later.
  53. final ViewStub viewStub = (ViewStub) view;
  54. viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
  55. }
  56. return view;
  57. }
  58. ......
  59. }

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

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

根据标签+属性创建View

两个关键点:

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

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

  1. View view;
  2. if (mFactory2 != null) {
  3. view = mFactory2.onCreateView(parent, name, context, attrs);
  4. } else if (mFactory != null) {
  5. view = mFactory.onCreateView(name, context, attrs);
  6. } else {
  7. view = null;
  8. }
  9. if (view == null && mPrivateFactory != null) {
  10. view = mPrivateFactory.onCreateView(parent, name, context, attrs);
  11. }
  12. //请注意下面
    声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/86855
    推荐阅读
    相关标签