动态换肤,插件化换肤的好处:不会增加apk包体积。无缝换肤。不用退出应用。不会闪屏。
思路:
1.如果要更换view的背景颜色,字体颜色,图片等,就必须先知道view的加载流程。
2.知道view的加载之后,就可以setTextColor,或者setBackroudColor了。
3.主要是setTextColor或者setBackroudColor的资源加载哪个地方的。默认是加载内置app
的。我们要做的事情就是换肤之后加载我们特定的换肤资源。
解决问题1:
activity里面view的加载流程。加载xml里面的view的过程。
默认使用的是LayoutInflater内部接口Factory里面的onCreateView进行创建view的。
系统提供了我们自定义加载view的Factory2。也是在LayoutInflater里面的一个接口。
activity默认使用Factory加载view。如果要使用Factory2加载view就得
在activity 的onCreate()方法中的 super.onCreate(savedInstanceState)之前调用
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater, this);方法进行设置Factory2
因为activity实现了LayoutInflater.Factory2接口。所以可以传入this。
接下来就是重写activity的 onCreateView方法。
activity默认使用 AppCompatViewInflater 类的 createView()方法去创建view。
现在我们要去拦截默认创建view的过程。只要继承AppCompatViewInflater 写一个子类
然后根据判断条件创建自己的view就可以把控view的颜色背景等的更换。
这里的name和attrs属性是为了后面找resourceId 和resourceName和resourceType用的。
里面的SkinnableTextView,SkinnableButton...是自己定义的目的是兼容
当我们需要换肤的时候,调用skinnableView()这个方法就可以换肤了。
问题:
这个换肤的思路:
首先我们要自定义一些attts属性。方便我们通过resourceId去获取Resource。
上面这个类是存储attrs属性的。我们取属性的时候从这个类里面取就可以了。
在创建view的方法里面把AttrsBean对象也创建并且赋值。
attrs文件里面具体的参数:下面只是一部分,后面可以自己添加。想要换Text string也是可以的。
需要换肤的view都去实现
ViewsMatch接口。这个接口只有一个方法。
public interface ViewsMatch { void skinnableView(); }
下面重点:
如何加载改变资源的路径??:
解决:
通过 AssetManager 里面的addAssetPath(String)方法进行资源路径的改变。
下面利用反射进行调用。
//创建资源管理器 AssetManager assetManager = AssetManager.class.newInstance(); //被@hide,目前只能反射去拿这个方法。 如果担心这个方法被@hide 可以反射addAssetPathInternal()方法。源码366行 + 387行 Method addAssetPath = assetManager.getClass().getDeclaredMethod(ADD_ASSET_PATH, String.class); //设置私有方法可访问 addAssetPath.setAccessible(true); //执行addAssetPath方法 addAssetPath.invoke(assetManager, skinPath);
下面是SkinManager 类的代码:
package com.example.skinlibrary;
import android.app.Application;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public final class SkinManager {
private static SkinManager instance;
private Application application;
private Resources appResources;//app内置资源
private Resources skinResources;//皮肤包的资源
private String skinPackageName;//皮肤包的包名
private boolean isDefaultSkin = true;//是否默认皮肤(app内置资源)
private static final String ADD_ASSET_PATH = "addAssetPath";//方法名
private Map<String ,SkinCache> cacheSkin;
private SkinManager(Application application) {
this.application = application;
appResources = application.getResources();
cacheSkin = new HashMap<>();
}
public static void init(Application application) {
if (instance == null) {
synchronized (SkinManager.class) {
if (instance == null) {
instance = new SkinManager(application);
}
}
}
}
public static SkinManager getInstance() {
return instance;
}
/**
* 加载皮肤包资源
*
* @param skinPath 皮肤包路径,为空则加载app内置资源
*/
public void loaderSkinResources(String skinPath) {
if (TextUtils.isEmpty(skinPath)) {
isDefaultSkin = true;
return;
}
if (cacheSkin.containsKey(skinPath)) {
isDefaultSkin = false;
SkinCache skinCache = cacheSkin.get(skinPath);
if (skinCache!=null){
skinPackageName = skinCache.getSkinPackageName();
skinResources = skinCache.getSkinResources();
return;
}
}
try {
//创建资源管理器
AssetManager assetManager = AssetManager.class.newInstance();
//被@hide,目前只能反射去拿这个方法。 如果担心这个方法被@hide 可以反射addAssetPathInternal()方法。源码366行 + 387行
Method addAssetPath = assetManager.getClass().getDeclaredMethod(ADD_ASSET_PATH, String.class);
//设置私有方法可访问
addAssetPath.setAccessible(true);
//执行addAssetPath方法
addAssetPath.invoke(assetManager, skinPath);
//创建加载外部皮肤包资源
skinResources = new Resources(assetManager,
appResources.getDisplayMetrics(), appResources.getConfiguration());
//根据皮肤包文件,获取皮肤包应用的包名
skinPackageName = application.getPackageManager()
.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;
//如果无法获取皮肤包应用的包名,加载app内置资源
//如果为空使用app内置资源,否则使用皮肤包。
isDefaultSkin = TextUtils.isEmpty(skinPackageName);
if (!isDefaultSkin){
cacheSkin.put(skinPath, new SkinCache(skinResources, skinPackageName));
}
} catch (Exception e) {
e.printStackTrace();
//预判:通过api加载可能有异常
isDefaultSkin = true;
}
}
/**
* 参考:resources.arsc 资源映射表
* 通过ID值获取资源的name和type
*
* @param resourceId 资源的ID值(app内置资源)
* @return 如果没有皮肤包则加载内置app资源id,反之则加载皮肤包对应的资源id
*/
private int getSkinResourceIds(int resourceId) {
//优化
if (isDefaultSkin) return resourceId;
String resourceName = appResources.getResourceEntryName(resourceId);// 资源的名字
String resourceTypeName = appResources.getResourceTypeName(resourceId);//drawable color
int skinResourceId = skinResources.getIdentifier(resourceName, resourceTypeName, skinPackageName);
//skinResourceId == 0证明没有在皮肤包里面找到这个资源。没找到就使用默认的。
isDefaultSkin = skinResourceId == 0;
return skinResourceId == 0 ? resourceId : skinResourceId;
}
public boolean isDefaultSkin() {
return isDefaultSkin;
}
//=============================================================================================
public int getColor(int resourceId) {
int ids = getSkinResourceIds(resourceId);
return isDefaultSkin ? appResources.getColor(ids) : skinResources.getColor(ids);
}
public ColorStateList getColorStateList(int resourceId) {
int ids = getSkinResourceIds(resourceId);
return isDefaultSkin ? appResources.getColorStateList(ids) : skinResources.getColorStateList(ids);
}
//mipmap和darwable统一用法
public Drawable getDrawableOrMipMap(int resourceId) {
int ids = getSkinResourceIds(resourceId);
return isDefaultSkin ? appResources.getDrawable(ids) : skinResources.getDrawable(ids);
}
public String getString(int resourceId) {
int ids = getSkinResourceIds(resourceId);
return isDefaultSkin ? appResources.getString(ids) : skinResources.getString(ids);
}
//返回特殊情况,可能是color drawable / mipmap
public Object getBackroudOrSrc(int resourceId) {
//需要获取当前属性的类型名,Resource.getResourceTypeName(resourceId)再判断
String resourceTypeName = appResources.getResourceTypeName(resourceId);
switch (resourceTypeName) {
case "color":
return getColor(resourceId);
case "mipmap":
case "drawable":
return getDrawableOrMipMap(resourceId);
}
return null;
}
}
这个管理类就可以替换资源了。在application里面初始化这个类。
public class BaseApplication extends Application { @Override public void onCreate() { super.onCreate(); SkinManager.init(this); } }
资源的路径是我们插件化创建新的apk的路径。新apk里面的res资源和原始app里面的资源名字要一一对应。
要换肤的就对应起来。不用换肤的不对应也没事。
下面是baseActivity的代码:
package com.example.skinlibrary;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.view.LayoutInflaterCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.example.skinlibrary.core.CustomAppCompatViewInflater;
import com.example.skinlibrary.utils.ActionBarUtils;
import com.example.skinlibrary.utils.NavigationBarUtils;
import com.example.skinlibrary.utils.StatusBarUtils;
public class SkinActivity extends AppCompatActivity implements UseSkin {
CustomAppCompatViewInflater mCustomAppCompatViewInflater;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater, this);
super.onCreate(savedInstanceState);
}
@Override
public boolean useSkin() {
return false;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
if (useSkin()) {
if (mCustomAppCompatViewInflater == null) {
mCustomAppCompatViewInflater = new CustomAppCompatViewInflater(context);
}
mCustomAppCompatViewInflater.setName(name);
mCustomAppCompatViewInflater.setAttrs(attrs);
return mCustomAppCompatViewInflater.autoMatch();
}
return super.onCreateView(name, context, attrs);
}
protected void setDayNightMode(@AppCompatDelegate.NightMode int nightMode) {
final boolean isPost21 = Build.VERSION.SDK_INT >= 21;
getDelegate().setLocalNightMode(nightMode);
if (isPost21) {
//换状态栏颜色
StatusBarUtils.forStatusBar(this);
//换标题栏颜色
ActionBarUtils.forActionBar(this);
//换导航栏颜色
NavigationBarUtils.forNavigationBar(this);
}
View decorView = getWindow().getDecorView();
applyDayNightForView(decorView);
}
/**
* 动态换肤(api限制5.0版本)
*
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
protected void defaultSkin(int temeColorId) {
this.skinDynamic(null, temeColorId);
}
/**
* 动态换肤(api限制5.0版本)
*
* @param skinPath
* @param themeColorId
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
protected void skinDynamic(String skinPath, int themeColorId) {
SkinManager.getInstance().loaderSkinResources(skinPath);
if (themeColorId != 0) {
int temeColorId = SkinManager.getInstance().getColor(themeColorId);
//换状态栏颜色
StatusBarUtils.forStatusBar(this, temeColorId);
//换标题栏颜色
ActionBarUtils.forActionBar(this, temeColorId);
//换导航栏颜色
NavigationBarUtils.forNavigationBar(this, temeColorId);
View decorView = getWindow().getDecorView();
applyDayNightForView(decorView);
}
}
/**
* 回调接口 给具体控件换肤操作
*
* @param decorView
*/
private void applyDayNightForView(View decorView) {
if (decorView instanceof ViewsMatch) {
ViewsMatch viewsMatch = (ViewsMatch) decorView;
viewsMatch.skinnableView();
}
if (decorView instanceof ViewGroup) {
ViewGroup parent = (ViewGroup) decorView;
int chidCount = parent.getChildCount();
for (int i = 0; i < chidCount; i++) {
applyDayNightForView(parent.getChildAt(i));
}
}
}
}
遍历一遍所有的view即可换肤了。
下面是测试的代码:
package com.example.skinproject;
import android.Manifest;
import android.annotation.TargetApi;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import com.example.skinlibrary.SkinActivity;
import java.io.File;
public class SkinTestActivity extends SkinActivity {
private String skinPath;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_skin_test);
skinPath = Environment.getExternalStorageDirectory() + File.separator + "net163.skin";
Log.d("SkinTestActivity", "路径=" + skinPath);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
String[] perms = {Manifest.permission.WRITE_EXTERNAL_STORAGE};
if (checkSelfPermission(perms[0]) == PackageManager.PERMISSION_DENIED) {
requestPermissions(perms, 200);
}
}
}
@Override
public boolean useSkin() {
return true;
}
//还原
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void def(View view) {
Log.d("SkinTestActivity", "time>>>>>>>>>");
long start = System.currentTimeMillis();
defaultSkin(R.color.colorPrimary);
Log.d("SkinTestActivity", "还原耗时= time>>>>>>>>>" + (System.currentTimeMillis() - start));
}
//换肤
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public void skin(View view) {
Log.d("SkinTestActivity", "time>>>>>>>>>");
long start = System.currentTimeMillis();
//第二个参数的主题色
skinDynamic(skinPath, R.color.skin_item_color);
Log.d("SkinTestActivity", "time>>>>>>>>>");
Log.d("SkinTestActivity", "换肤耗时= time>>>>>>>>>" + (System.currentTimeMillis() - start));
}
}
6.0需要申请权限。因为皮肤包是放在apk外部的。
源码路径:https://download.csdn.net/download/qq_40207976/16660733