赞
踩
可以先看前几篇文章:
Android 动态式换肤框架1-setContentView源码分析:
https://blog.csdn.net/hongxue8888/article/details/95494195
Android 动态式换肤框架2-实现背景替换:
https://blog.csdn.net/hongxue8888/article/details/95390639
Android 动态式换肤框架3-Fragment、状态栏换肤:
https://blog.csdn.net/hongxue8888/article/details/96310094
Android 动态式换肤框架4-自定义控件换肤:
https://blog.csdn.net/hongxue8888/article/details/96476296
先看效果图:
这里使用了两个字体,分别标记为typeface和typeface2。其中typeface用于全局字体替换,typeface2字体替换需要手动去设置。
新建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当中
我们在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">
在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; } }
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方法。
共享首选项,用于记录当前使用的皮肤。
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); } }
前面在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); } }
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了。
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,这就是我们皮肤包的字体。
接下来根据流程来讲解代码,从点击换肤按钮开始讲起。
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>
前面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是一个被观察者,这里通知观察者去更新
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。