当前位置:   article > 正文

Android 常用换肤方式以及原理分析_android applystyle

android applystyle

原文地址:https://juejin.im/post/5b8f6dcde51d450e6a2dcadf

原文内容:

常用方法

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>
  15. 复制代码

设置主要切换主题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>
  18. 复制代码

切换主题:

通过调用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);
  12. 复制代码

效果如下:

 

 

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));
  9. 复制代码

加载未安装应用的资源

  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));
  20. 复制代码

LayoutInflater.Factory

分析setContentView源码

LayoutInflater.Factory是如何被调用的

 

setContentView最终调用了inflate方法,我们来看一下inflate方法的源码

 

 

inflate最终调用了createViewFromTag方法来创建View,在这之中用到了factory,如果factory存在就用factory创建对象,如果不存在就由系统自己去创建

 

 

我们在setContentView之前调用测试代码 测试代码:

  1. LayoutInflater.from(this).setFactory(new LayoutInflater.Factory() {
  2. @Override
  3. public View onCreateView(String name, Context context, AttributeSet attrs) {
  4. Log.e("MainActivity", "name :" + name);
  5. int count = attrs.getAttributeCount();
  6. for (int i = 0; i < count; i++) {
  7. Log.e("MainActivity", "AttributeName :" + attrs.getAttributeName(i) + "AttributeValue :"+ attrs.getAttributeValue(i));
  8. }
  9. return null;
  10. }
  11. });
  12. 复制代码

log日志:

 

 

结果发现我们可以获取一个layout的所有View,此时我们就可以对View进行皮肤切换效果。

通过AssetManager切换主题总结

通过AssetManager和LayoutInflater.Factory配合就可以达到调用外部资源获取皮肤的方法。如果想要动态更新,只需要把需要动态更新的View存起来,去遍历设置皮肤,或者用eventBus去通知也可以。

对比

上述两种方法是市面上大多数换肤框架的实现原理。
通过Theme切换主题:
优点:实现简单,配置简单
缺点:需要重启应用;是固定皮肤,不能动态切换
通过AssetManager切换主题:
优点:不需要重启应用;可以动态加载主题,用于盈利 缺点:实现较为复杂;皮肤包比较占资源

项目地址:github.com/JavaNoober/…

 

 

其实在我们的浏览器项目中实现的日夜间模式的切换,也是属于皮肤切换的一种。支持动态切换,不需要重启,实现相对复杂。每种类型的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都要实现自定义的属性,不便于扩展。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号