Android 动态式换肤框架5-字体换肤

可以先看前几篇文章:

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字体替换需要手动去设置。


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是一个被观察者,这里通知观察者去更新

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值