赞
踩
原文地址:https://juejin.im/post/5b8f6dcde51d450e6a2dcadf
通过在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加载皮肤包里面的资源文件,实现资源替换。
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));
- 复制代码
分析setContentView源码
LayoutInflater.Factory是如何被调用的
setContentView最终调用了inflate方法,我们来看一下inflate方法的源码
inflate最终调用了createViewFromTag方法来创建View,在这之中用到了factory,如果factory存在就用factory创建对象,如果不存在就由系统自己去创建。
我们在setContentView之前调用测试代码 测试代码:
- LayoutInflater.from(this).setFactory(new LayoutInflater.Factory() {
- @Override
- public View onCreateView(String name, Context context, AttributeSet attrs) {
- Log.e("MainActivity", "name :" + name);
- int count = attrs.getAttributeCount();
- for (int i = 0; i < count; i++) {
- Log.e("MainActivity", "AttributeName :" + attrs.getAttributeName(i) + "AttributeValue :"+ attrs.getAttributeValue(i));
- }
- return null;
- }
- });
- 复制代码
log日志:
结果发现我们可以获取一个layout的所有View,此时我们就可以对View进行皮肤切换效果。
通过AssetManager切换主题总结
通过AssetManager和LayoutInflater.Factory配合就可以达到调用外部资源获取皮肤的方法。如果想要动态更新,只需要把需要动态更新的View存起来,去遍历设置皮肤,或者用eventBus去通知也可以。
上述两种方法是市面上大多数换肤框架的实现原理。
通过Theme切换主题:
优点:实现简单,配置简单
缺点:需要重启应用;是固定皮肤,不能动态切换
通过AssetManager切换主题:
优点:不需要重启应用;可以动态加载主题,用于盈利 缺点:实现较为复杂;皮肤包比较占资源
其实在我们的浏览器项目中实现的日夜间模式的切换,也是属于皮肤切换的一种。支持动态切换,不需要重启,实现相对复杂。每种类型的view都需要自定义,只所以需要自定义是需要加载自定义的属性,该属性是一个style集合,分别对应了日夜间模式所使用的style。比如这样:
<com.android.browser.view.BrowserTextView android:id="@+id/tv_browser_guide_view_cancel" android:layout_centerHorizontal="true" android:fontFamily="sans-serif-medium" android:gravity="center" android:text="@string/guide_view_uc_cancel_btn_text" android:layout_alignParentBottom="true" android:textSize="@dimen/guide_view_btn_text_size" android:layout_marginBottom="@dimen/guide_view_cancel_bottom_margin" android:layout_width="@dimen/guide_view_btn_width" android:layout_height="@dimen/guide_view_btn_height" browser:browserViewTheme="@style/guide_view_btn_theme"/>
browserViewTheme就是一个自定义属性,引入了两种style,夜间和日间:
<style name="guide_view_btn_theme"> <item name="theme_default">@style/guide_view_btn_theme_day</item> <item name="theme_custom">@style/guide_view_btn_theme_night</item> </style>
具体的夜间和日间的style就是具体的原生view的属性值,比如:
<style name="guide_view_btn_theme_day"> <item name="android:textColor">@color/guide_view_blue_color</item> <item name="android:background">@drawable/btn_guide_view_cancel_selector</item> </style>
然后在所有的自定义的view中都实现了一个叫ThemeableView的接口
public interface ThemeableView { public static final String THEME_CUSTOM = "custom"; //现用于夜间模式。 public static final String THEME_DEFAULT = "default"; //普通模式。 public static final String THEME_MENU_PAGE = "menu_page"; //多任务界面模式。 public void applyTheme(String whichTheme); public void addTheme(String whichTheme, int styleId); }
关键方法就是applyTheme,在这个方法中根据当前的模式去加载style,然后对每个属性设置对应的值,比如在BrowserTextView中,
public void applyTheme(String whichTheme) { if (whichTheme.equals(mCurrentTheme)) { return; } mCurrentTheme = whichTheme; int styleId = 0; Integer tmp = mThemeSet.get(mCurrentTheme); if (tmp != null && tmp != 0) { styleId = tmp; } if (styleId != 0) { ThemeUtils.applyStyle_View(this, styleId); ThemeUtils.applyStyle_TextView(this, styleId); ThemeUtils.applyStyle_BrowserTextView(this, null, styleId); } }
其中ThemUtils.applyStyle_BrowserTextView方法是这样的:
public static void applyStyle_BrowserTextView(BrowserTextView v, AttributeSet set, int styleId) { TypedArray a = v.getContext().getTheme().obtainStyledAttributes(set, R.styleable.BrowserTextView, 0, styleId); int N = a.getIndexCount(); for (int i = 0; i < N; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.BrowserTextView_selected_color: int color = a.getColor(attr, 0); v.setSelectedTextColor(color); break; case R.styleable.BrowserTextView_unselected_color: int uncolor = a.getColor(attr, 0); v.setUnSelectedTextColor(uncolor); break; case R.styleable.BrowserTextView_drawable_left_selected_color: int drawableLeftSelectedColor = a.getColor(attr, 0); v.setSelectedDrawableLeftColor(drawableLeftSelectedColor); break; case R.styleable.BrowserTextView_drawable_left_unselected_color: int drawableLeftUnselectedColor = a.getColor(attr, 0); v.setUnselectedDrawableLeftColor(drawableLeftUnselectedColor); break; case R.styleable.BrowserTextView_selected: boolean selected = a.getBoolean(attr, false); v.setMzSelected(selected); break; case R.styleable.BrowserTextView_background_sets: int bgSetsId = a.getResourceId(attr, 0); if (bgSetsId != 0) { v.setBackgroundSets(bgSetsId); } break; case R.styleable.BrowserTextView_current_background: String whichName = a.getString(attr); v.setCurrentBackground(whichName); break; default: break; } } a.recycle(); }
解析attributeSet属性,然后重新给这个view设置各个属性值。设置新的值之后view就开始重绘,这样就动态的切换了背景或者前景,或者字体颜色之类的了。
之所以每个view都实现ThemeableView接口是方便在需要更换日夜间模式的时候可以遍历所有的view的applyThme方法来达到变更所以当前在界面上的view的模式。
总的来说,该实现方式相较于上面两种方式来说相对复杂些,牵扯的类也比较多,所有view都要实现自定义的属性,不便于扩展。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。