很多app都会设置夜间和白天的模式,而实现换肤的方法有很多种,有的必须重新进入才能有效果,有的是动态的,设置了就马上就可以显示。
首先看看通过设置主题的方式来实现换肤
通过设置setTheme(R.style.BlackTheme);来改变字体颜色,背景灯
<style name="Theme.MyApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
</style>
可以设置多个主题,然后在进入activity改变主题,但是这中方法,不能时时有效,必须重新进入才能。
所以实际项目中运用更多的是动态换肤。
换皮肤我们需要解决的问题是找到view对象,然后和获取到需要替换的资源文件
第一步获取需要换肤的view,
一种是在Activity的oncreat方法setContentView方法后获取view,但是此时view
已经加载了,我们再去获取修改,就要重新设置一次,而且每个activity都要写大量代码,体验和性能都不好。
我们都知道activity加载布局文件是在
setContentView里添加的。无论是继承 AppCompatActivity 或者Activity最后
都是会执行到LayoutInflater.from(mContext).inflate(resId, contentParent);
然后这里面就是循环解析xml文件最后会执行到
public final View tryCreatView(View parent,String name,Context context,AttributeSet attrs){
if(mFactoty2!=null){
view =mFactoty2.onCreatView(parent,name,context,attrs)
}else if(mFactory != null){
view =mFactoty.onCreatView(name,context,attrs)
}
if(view ==null && mPrivateFacory){
view =mPrivateFacory.onCreatView(parent,name,context,attrs)
}
}
可以看到最终会执行到这个地方,这里面默认mFactoty2 和mFactory 都是空
最后执行到mPrivateFacory 这个地方,但是在这里没有看到这个new出来,但是我们看activity的启动时候在ActivityThread里面的的activity.attach
方法mwindow.getLayoutInflater.setprivateFactory(this),设置了,所以如果我们想
在可以在getLayoutInflater ,设置factory2活着factory去在我们自定义的LayoutInflater.factory 里面去获取view去设置对应的资源属性。
然后重写这个方法。
public View onCreateView(@Nullable View view, @NonNull String name, @NonNull Context context, @NonNull AttributeSet set) {
Log.d("jun","------>"+name);
View realView= null;
if (name.contains(".")) {//表示不是常用的TextView这些控件,不包括自定义,
// 第三方。v7 ,v4,androidx等库的控件,这些需要单独去适配这里只是说原理,其他的都是差不多的
realView = createView(name, context, set);
} else {//系统控件
for (int i = 0; i < sClassPrefixList.length; i++) {
realView = createView(sClassPrefixList[i] + name, context, set);
if (realView != null) {
break;
}
}
}
List<SkinAttr> skinAttrsList = new ArrayList<>();
for (int i=0;i<set.getAttributeCount();i++){
String attributeName = set.getAttributeName(i);//属性的名字background
String attributeValue = set.getAttributeValue(i);//属性的值
//在这里收集的属性主要是皮肤换肤需要的一些属性,例如background,textColor,src等
if(isSupportSkinAttr(attributeName)){
//资源的id,实际就是R文件的id
int resId = Integer.parseInt(attributeValue.substring(1));//截取@2131361811 ,拿到实际的在R文件中的值
String resName = context.getResources().getResourceName(resId);//这个是完整的路径
String res = context.getResources().getResourceEntryName(resId);//资源的名字
String attrType = context.getResources().getResourceTypeName(resId);
Log.i("jun","res"+res+"name:"+resName+"----attrType"+attrType+"---rId");
SkinAttr attr = new SkinAttr(attributeName,attrType,res,resId);
skinAttrsList.add(attr);
}
}
SkinView skinView = new SkinView(view,skinAttrsList);
skinViews.add(skinView);
skinView.skinApply();
return realView;
}
这样我们获取了所有的view对象和它的属性
然后需要解决的就是如何获取到我们的替换资源。
我们都知道资源的获取是通过context.getResource获取的。
而context是在什么时候创建的了,我们在看activity启动流程时候
在ActivityThread的performlaunchActivity方法中
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
ComponentName component = r.intent.getComponent();
if (component == null) {
component = r.intent.resolveActivity(
mInitialApplication.getPackageManager());
r.intent.setComponent(component);
}
if (r.activityInfo.targetActivity != null) {
component = new ComponentName(r.activityInfo.packageName,
r.activityInfo.targetActivity);
}
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
//下面省略
}
createBaseContextForActivity 会创建basecontext ,可以看出实际 的创建是在
ContextImpl 这里完成的 走到了 createActivityContext 这个方法
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
Configuration overrideConfiguration) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
String[] splitDirs = packageInfo.getSplitResDirs();
ClassLoader classLoader = packageInfo.getClassLoader();
//省略
context.setResources(resourcesManager.createBaseTokenResources(activityToken,
packageInfo.getResDir(),
splitDirs,
packageInfo.getOverlayDirs(),
packageInfo.getOverlayPaths(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
classLoader,
packageInfo.getApplication() == null ? null
: packageInfo.getApplication().getResources().getLoaders()));
}
这里设置资源可以得知resourcesManager.createBaseTokenResources( 这里面创建的
最后走到resourcesManager 的
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key,
@Nullable ApkAssetsSupplier apkSupplier) {
final AssetManager assets = createAssetManager(key, apkSupplier);
if (assets == null) {
return null;
}
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
final Configuration config = generateConfig(key);
final DisplayMetrics displayMetrics = getDisplayMetrics(generateDisplayId(key), daj);
final ResourcesImpl impl = new ResourcesImpl(assets, displayMetrics, config, daj);
if (DEBUG) {
Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
}
return impl;
}
我们看到这个点地方会创建AssetManager ,不过33版本的创建Assertmanger 方式改变了,
private @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key,
@Nullable ApkAssetsSupplier apkSupplier) {
final AssetManager.Builder builder = new AssetManager.Builder();
final ArrayList<ApkKey> apkKeys = extractApkKeys(key);
for (int i = 0, n = apkKeys.size(); i < n; i++) {
final ApkKey apkKey = apkKeys.get(i);
try {
builder.addApkAssets(
(apkSupplier != null) ? apkSupplier.load(apkKey) : loadApkAssets(apkKey));
} catch (IOException e) {
if (apkKey.overlay) {
Log.w(TAG, String.format("failed to add overlay path '%s'", apkKey.path), e);
} else if (apkKey.sharedLib) {
Log.w(TAG, String.format(
"asset path '%s' does not exist or contains no resources",
apkKey.path), e);
} else {
Log.e(TAG, String.format("failed to add asset path '%s'", apkKey.path), e);
return null;
}
}
}
if (key.mLoaders != null) {
for (final ResourcesLoader loader : key.mLoaders) {
builder.addLoader(loader);
}
}
return builder.build();
}
之前的是调用AssetManager的这个方法去把资源路径传递
public int addAssetPath(String path) {
}
把path传到apkkey里面然后添加到这个方法
新版的是 builder.addApkAssets( apkkey)
不过原来的addAssetPath被标为过时的,还可以用。
我们这个地方还是可以按照
首先获取 资源文件
public boolean loadSkin(String skinPath) {
//------------拿到skinPackageName----------
boolean isSuccess =false;
PackageInfo packageArchiveInfo = mContext.getPackageManager().getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
if (packageArchiveInfo == null) {
} else {
//----------拿到skin中的Resource对象----------
AssetManager assets = null;
skinPackageName = packageArchiveInfo.packageName;
try {
assets = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assets, skinPath);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
mChooseResources = new Resources(assets, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
isSuccess =true;
}
return isSuccess;
}
然后将这个mChooseResources 保存,换肤获取资源就通过这个去获取。
结合上面获取到的view,就可以直接进行换肤了。
demo 在这里 代码.
网友评论