可以先看前几篇文章:
Android 动态式换肤框架1-setContentView源码分析:
Android 动态式换肤框架2-实现背景替换:
Android 动态式换肤框架3-Fragment、状态栏换肤:
Android 动态式换肤框架4-自定义控件换肤:
文章目录
- 1 准备工作
- 1.1 皮肤包制作
- 1.2 皮肤包和app的连接桥梁
- 1.3 下载皮肤包
- 1.4 SkinManager皮肤管理类
- 1.5 SkinPreference记录当前使用的皮肤
- 2 采集需要换肤的控件
- 2.1 SkinActivityLifecycle
- 2.2 SkinThemeUtils
- 2.3 SkinResources
- 2 换肤流程
- 2.1 SkinActivity
- 2.2 SkinManager的loadSkin方法
先看效果图:
这里使用了两个字体,分别标记为typeface和typeface2。其中typeface用于全局字体替换,typeface2字体替换需要手动去设置。
1 准备工作
1.1 皮肤包制作
新建app_skin Module作为皮肤包,如下图所示:
global.ttf和specified.ttf 是需要使用的字体文件。
然后在strings.xml 中添加
<string name="typeface">font/global.ttf</string>
<string name="typeface2">font/specified.ttf</string>
make project后将生成apk拷贝到app当中assets当中
1.2 皮肤包和app的连接桥梁
我们在app的strings.xml文件中也添加两个名称相同的string标签,只是没有值,如下:
<string name="typeface"/>
<string name="typeface2"/>
如果想更换字体,我们就要想办法将通过这两个string,去皮肤包中找到相同名称的string,就是通过这两个string使皮肤包和app建立了连接关系。
还有一点如果想全局TextView都能自动更换字体,我们需要在styles.xml的AppTheme添加一个名为skinTypeface的item。
<!--styles.xml-->
<resources>
<style name="AppTheme" 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="skinTypeface">@string/typeface</item>
</style>
</resources>
配置文件中在application节点下添加AppTheme:
<application
...
android:theme="@style/AppTheme">
1.3 下载皮肤包
在MainActivity模拟皮肤包的下载,并保存路径到MyApplication中的apkPath中。
public class MainActivity extends AppCompatActivity {
String apkName = "app_skin-debug.apk";
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
try {
Utils.extractAssets(newBase, apkName);
} catch (Throwable e) {
e.printStackTrace();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
File extractFile = this.getFileStreamPath(apkName);
String apkPath = extractFile.getAbsolutePath();
MyApplication.getApplication().setApkPath(apkPath);
}
public void skinSelect(View view) {
startActivity(new Intent(this, SkinActivity.class));
}
}
public class Utils {
/**
* 把Assets里面得文件复制到 /data/data/files 目录下
*/
public static void extractAssets(Context context, String sourceName) {
AssetManager am = context.getAssets();
InputStream is = null;
FileOutputStream fos = null;
try {
is = am.open(sourceName);
File extractFile = context.getFileStreamPath(sourceName);
fos = new FileOutputStream(extractFile);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
closeSilently(is);
closeSilently(fos);
}
}
private static void closeSilently(Closeable closeable) {
if (closeable == null) {
return;
}
try {
closeable.close();
} catch (Throwable e) {
// ignore
}
}
}
MyApplication除了保存皮肤包路径外,对调用了SkinManager(皮肤管理类)的init方法对其进行了初始化操作,代码如下:
public class MyApplication extends Application {
private static MyApplication myApplication = null;
public static MyApplication getApplication(){
if (myApplication == null){
myApplication = new MyApplication();
}
return myApplication;
}
String apkPath;
@Override
public void onCreate() {
super.onCreate();
SkinManager.init(this);
}
public String getApkPath() {
return apkPath;
}
public void setApkPath(String apkPath) {
this.apkPath = apkPath;
}
}
1.4 SkinManager皮肤管理类
public class SkinManager extends Observable {
private static SkinManager instance;
private Application application;
public static void init(Application application){
synchronized (SkinManager.class) {
if(null == instance){
instance = new SkinManager(application);
}
}
}
public static SkinManager getInstance() {
return instance;
}
private SkinManager(Application application) {
this.application = application;
//共享首选项 用于记录当前使用的皮肤
SkinPreference.init(application);//1
//资源管理类 用于从app/皮肤 中加载资源
SkinResources.init(application);//2
/**
* 提供了一个应用生命周期回调的注册方法,
* * 用来对应用的生命周期进行集中管理,
* 这个接口叫registerActivityLifecycleCallbacks,可以通过它注册
* * 自己的ActivityLifeCycleCallback,每一个Activity的生命周期都会回调到这里的对应方法。
*/
application.registerActivityLifecycleCallbacks(new SkinActivityLifecycle());//3
loadSkin(SkinPreference.getInstance().getSkin());//4
}
public void loadSkin(String path) {
if(TextUtils.isEmpty(path)){
// 记录使用默认皮肤
SkinPreference.getInstance().setSkin("");
//清空资源管理器, 皮肤资源属性等
SkinResources.getInstance().reset();
} else {
try {
//反射创建AssetManager
AssetManager manager = AssetManager.class.newInstance();
// 资料路径设置 目录或者压缩包
Method addAssetPath = manager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(manager, path);
Resources appResources = this.application.getResources();
Resources skinResources = new Resources(manager,
appResources.getDisplayMetrics(), appResources.getConfiguration());
//记录
SkinPreference.getInstance().setSkin(path);
//获取外部Apk(皮肤薄) 包名
PackageManager packageManager = this.application.getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
String packageName = packageArchiveInfo.packageName;
SkinResources.getInstance().applySkin(skinResources,packageName);
} catch (Exception e) {
e.printStackTrace();
}
}
//采集的view 皮肤包
setChanged();//5
//通知观者者
notifyObservers();//6
}
}
注释1:初始化自定义的SharedPreference
注释2:初始化SkinResources(资源管理类)
注释3:注册自定义的SkinActivityLifecycle
注释4:如果更换过皮肤,进入后加载新皮肤
注释5和6:通知观察者
后面会有点击换肤按钮的操作,调用的也是loadSkin方法。
1.5 SkinPreference记录当前使用的皮肤
共享首选项,用于记录当前使用的皮肤。
SkinPreference的setSkin和getSkin方法用于保存和获取皮肤包的路径。点击换肤按钮的时候,会将皮肤包路径保存到SharePreference当中,表明换肤过;如果点击还原按钮,则会保存为null。
public class SkinPreference {
private static final String SKIN_SHARED = "skins";
private static final String KEY_SKIN_PATH = "skin-path";
private static SkinPreference instance;
private final SharedPreferences mPref;
public static void init(Context context) {
if (instance == null) {
synchronized (SkinPreference.class) {
if (instance == null) {
instance = new SkinPreference(context.getApplicationContext());
}
}
}
}
public static SkinPreference getInstance() {
return instance;
}
private SkinPreference(Context context) {
mPref = context.getSharedPreferences(SKIN_SHARED, Context.MODE_PRIVATE);
}
public void setSkin(String skinPath) {
mPref.edit().putString(KEY_SKIN_PATH, skinPath).apply();
}
public String getSkin() {
return mPref.getString(KEY_SKIN_PATH, null);
}
}
2 采集需要换肤的控件
2.1 SkinActivityLifecycle
前面在SkinManager中可以看到注册了一个SkinActivityLifecycle,SkinActivityLifecycles实现了Application.ActivityLifecycleCallbacks接口,使用ActivityLifecycleCallbacks对应用的生命周期进行集中管理。每次进入一个Activity时都会调用onActivityCreated方法,我们在
public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
HashMap<Activity , SkinLayoutFactory> factoryHashMap = new HashMap<>();
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
/**
* 更新字体
*/
Typeface skinTypeface = SkinThemeUtils.getSkinTypeface(activity);
LayoutInflater layoutInflater = LayoutInflater.from(activity);
try {
Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");
mFactorySet.setAccessible(true);
mFactorySet.setBoolean(layoutInflater, false);
} catch (Exception e) {
e.printStackTrace();
}
//添加自定义创建View 工厂
SkinLayoutFactory factory = new SkinLayoutFactory(activity,skinTypeface);
layoutInflater.setFactory2(factory);
//注册观察者
SkinManager.getInstance().addObserver(factory);
factoryHashMap.put(activity, factory);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
//删除观察者
SkinLayoutFactory remove = factoryHashMap.remove(activity);
SkinManager.getInstance().deleteObserver(remove);
}
}
2.2 SkinThemeUtils
public class SkinThemeUtils {
private static int[] TYPEFACE_ATTRS = {//1
R.attr.skinTypeface
};
public static int[] getResId(Context context, int[] attrs){
int[] ints = new int[attrs.length];
TypedArray typedArray = context.obtainStyledAttributes(attrs);
for (int i = 0; i < typedArray.length(); i++) {
ints[i] = typedArray.getResourceId(i, 0);
}
typedArray.recycle();
return ints;
}
public static Typeface getSkinTypeface(Activity activity) {
//获取字体id
int skinTypefaceId = getResId(activity, TYPEFACE_ATTRS)[0];//2
return SkinResources.getInstance().getTypeface(skinTypefaceId);
}
}
注释1:在attr.xml中定义的<attr name=“skinTypeface” format=“string”/>
注释2:通过getResId方法得到skinTypefaceId=2131427370
打开R.class,发现typeface = 2131427370,如下:
public static final class string {
public static final int typeface = 2131427370;
public static final int typeface2 = 2131427371;
}
这是因为在 styles.xml中设置了skinTypeface的值为typeface的值,如下:
<item name="skinTypeface">@string/typeface</item>
这样自定义的属性就可以使用皮肤包中的typeface了。
2.3 SkinResources
public class SkinResources {
private static SkinResources instance;
private Resources mSkinResources;
private String mSkinPkgName;
private boolean isDefaultSkin = true;
private Resources mAppResources;
private SkinResources(Context context) {
mAppResources = context.getResources();
}
public static void init(Context context) {
if (instance == null) {
synchronized (SkinResources.class) {
if (instance == null) {
instance = new SkinResources(context);
}
}
}
}
public static SkinResources getInstance() {
return instance;
}
public void reset() {
mSkinResources = null;
mSkinPkgName = "";
isDefaultSkin = true;
}
public void applySkin(Resources resources, String pkgName) {
mSkinResources = resources;
mSkinPkgName = pkgName;
//是否使用默认皮肤
isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;
}
public int getIdentifier(int resId) {//1
if (isDefaultSkin) {
return resId;
}
//在皮肤包中不一定就是 当前程序的 id
//获取对应id 在当前的名称 colorPrimary
//R.drawable.ic_launcher
String resName = mAppResources.getResourceEntryName(resId);//ic_launcher /colorPrimaryDark
String resType = mAppResources.getResourceTypeName(resId);//drawable
//使用getIdentifier()方法可以方便的获各应用包下的指定资源ID。
// 第一个参数为ID名,我们定义的名称为typeface,
// 第二个为资源属性如string,
// 第三个为包名。
int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);//2
return skinId;
}
public int getColor(int resId) {
if (isDefaultSkin) {
return mAppResources.getColor(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColor(resId);
}
return mSkinResources.getColor(skinId);
}
public ColorStateList getColorStateList(int resId) {
if (isDefaultSkin) {
return mAppResources.getColorStateList(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColorStateList(resId);
}
return mSkinResources.getColorStateList(skinId);
}
public Drawable getDrawable(int resId) {
//如果有皮肤 isDefaultSkin false 没有就是true
if (isDefaultSkin) {
return mAppResources.getDrawable(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getDrawable(resId);
}
return mSkinResources.getDrawable(skinId);
}
/**
* 可能是Color 也可能是drawable
*
* @return
*/
public Object getBackground(int resId) {
String resourceTypeName = mAppResources.getResourceTypeName(resId);
if (resourceTypeName.equals("color")) {
return getColor(resId);
} else {
// drawable
return getDrawable(resId);
}
}
/**
* Typeface 字体对象
*
* @param skinTypefaceId 属性id
*/
public Typeface getTypeface(int skinTypefaceId) {
String skinTypefacePath = getString(skinTypefaceId);
if (TextUtils.isEmpty(skinTypefacePath)) {
return Typeface.DEFAULT;
}
try {
if (isDefaultSkin) {
return Typeface.createFromAsset(mAppResources.getAssets(), skinTypefacePath);
}
return Typeface.createFromAsset(mSkinResources.getAssets(), skinTypefacePath);//4
} catch (Exception e) {
}
return Typeface.DEFAULT;
}
private String getString(int skinTypefaceId) {
try {
//使用默认皮肤
if (isDefaultSkin) {
//使用app 设置的属性值
return mAppResources.getString(skinTypefaceId);
}
int skinId = getIdentifier(skinTypefaceId);
if (skinId == 0) {
//使用app 设置的属性值
return mAppResources.getString(skinTypefaceId);
}
return mSkinResources.getString(skinId);//3
} catch (Exception e) {
}
return null;
}
}
注释1:首先会调用getIdentifier方法,通过resId可以获取到resName和resType。
resId=2131427370,
resName=typeface,
resType=string,
mSkinPkgName=com.hongx.skinplugin//这是皮肤包的包名注释2:使用getIdentifier()方法可以方便的获各应用包下的指定资源ID。
// 第一个参数为ID名,我们定义的名称为typeface,
// 第二个为资源属性如string,
// 第三个为包名。
这样就获取到了皮肤包中typeface的值,skinId=2131361833
我们可以看下皮肤包的R.class文件:
public static final class string {
public static final int typeface = 2131361833;
public static final int typeface2 = 2131361834;
}
注释3:通过SkinResources的getString方法就获取到了skinTypefacePath的值
skinTypefacePath=font/global.ttf ,这样就找到了皮肤包的字体文件路径。注释4:通过Typeface.createFromAsset 创建了Typeface,这就是我们皮肤包的字体。
2 换肤流程
接下来根据流程来讲解代码,从点击换肤按钮开始讲起。
2.1 SkinActivity
public class SkinActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_skin);
}
public void change(View view) {//1
String path = MyApplication.getApplication().getApkPath();//2
SkinManager.getInstance().loadSkin(path);//3
}
public void restore(View view) {//4
SkinManager.getInstance().loadSkin(null);
}
}
注释1:change方法为换肤的点击事件
注释2:皮肤包的路径
注释3:加载皮肤包。loadSkin方法具体看前面的SkinManager
注释4:点击还原按钮操作,只需在loadSkin方法中传入一个null即可。
activity_skin.xml为SkinActivity的布局文件,代码如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="change"
android:text="换肤"/>
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="restore"
android:text="还原"/>
</LinearLayout>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="我是一个Button"
android:textSize="22sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="我是一个TextView"
android:textSize="22sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:layout_marginTop="20dp"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:text="测试TextView"
android:textSize="22sp"
android:textColor="@color/colorAccent"
android:typeface="normal"/>
<!--注释1-->
<TextView
android:layout_marginTop="10dp"
android:textSize="22sp"
skinTypeface="@string/typeface2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="我使用了 typeface2"
tools:ignore="MissingPrefix" />
</LinearLayout>
注释1:skinTypeface="@string/typeface2" 指定了使用第二种字体,即specified.ttf字体
skinTypeface为自定义属性,需要在attrs.xml文件中添加,如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="skinTypeface" format="string"/>
</resources>
2.2 SkinManager的loadSkin方法
前面SkinActivity的注释3调用了SkinManager的loadSkin方法,SkinManager前面已经有介绍,这里单独把loadSkin方法拿出来分析。
public void loadSkin(String path) {
if(TextUtils.isEmpty(path)){//1
// 记录使用默认皮肤
SkinPreference.getInstance().setSkin("");
//清空资源管理器, 皮肤资源属性等
SkinResources.getInstance().reset();
} else {
try {
//反射创建AssetManager
AssetManager manager = AssetManager.class.newInstance();
// 资料路径设置 目录或者压缩包
Method addAssetPath = manager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(manager, path);
Resources appResources = this.application.getResources();
Resources skinResources = new Resources(manager,
appResources.getDisplayMetrics(), appResources.getConfiguration());//1
//记录
SkinPreference.getInstance().setSkin(path);
//获取外部Apk(皮肤薄) 包名
PackageManager packageManager = this.application.getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
String packageName = packageArchiveInfo.packageName;//2
SkinResources.getInstance().applySkin(skinResources,packageName);//3
} catch (Exception e) {
e.printStackTrace();
}
}
//采集的view 皮肤包
setChanged();
//通知观者者
notifyObservers();//4
}
注释1:通过AssetManager和app的Resources可以获取到皮肤包的Resources(skinResources)
注释2:获取到皮肤包的包名(packageName)
注释3:将皮肤包的Resources和packageName保存到SkinResources当中,后面会用到
注释4:SkinManager继承Observable是一个被观察者,这里通知观察者去更新