基于之前的两篇文章 获取其他apk中的资源 以及 view创建的拦截
我们可以搭建自己的换肤框架了
1.架构分析
SkinType:表示换肤的资源类型 ,skin()实现了具体换肤的方法
SkinAttr:存储了某个view的一个可以换肤的属性,存储的值为 当前资源名+属性类型
SkinView:存储切换皮肤的一个view 该view可能存在多个属性需要换肤applySkin用于遍历当前view的可换肤属性 然后对view进行换肤
SkinManager:核心类 换肤公开接口调用的地方 内部保存mSkinResource对象,初始化时缓存了所有可以换肤的view对象,需要换肤时取出这些对象 进行换肤
SkinResource:核心类 包含Resources对象 可以根apk path和packageName生成不同的Resources对象从而获取不同的皮肤包(apk)中的资源(color drawable等)
BaseSkinActivity:继承自AppCompatActivity 覆盖onCreate方法 模仿Android源码 在onCreate方法中调用LayoutInflaterCompat.setFactory2 然后在onCreateView中拦截view的创建
SkinAttrSupport:BaseSkinActivity的辅助类 SkinAttrSupport负责解析AttributeSet并构建List对象 并交由BaseSkinActivity存储为可以换肤的view对象
SkinAppCompatViewInflater:拷贝自Android源码 BaseSkinActivity的辅助类SkinAppCompatViewInflater负责在内部拦截view的创建
ActivityChangeSkin:具体调用换肤方法的activity
SkinPathUtil:辅助类 用于获取皮肤包路径
2.搭建框架
搭建顺序从上图的SkinType倒过来搭建
/**
* Created by hjcai on 2021/4/14.
* 需要换肤的类型
* 主要有:android:src android:background android:textColor android:textColorHint等 没有列出的可以自己补充
*/
public enum SkinType {
TEXT_COLOR("textColor") {
@Override
public void skin(View view, String resName) {
if (view == null){
return;
}
SkinResource skinResource = getSkinResource();
ColorStateList color = skinResource.getColorStateListByName(resName);
if (color == null) {
return;
}
TextView textView = (TextView) view;
textView.setTextColor(color);
}
},
BACKGROUND("background") {
@Override
public void skin(View view, String resName) {
if (view == null){
return;
}
// background 是图片的情况
SkinResource skinResource = getSkinResource();
Drawable drawable = skinResource.getDrawableByName(resName);
if (drawable != null) {
view.setBackground(drawable);
return;
}
int color = skinResource.getColorByName(resName);
if (color == 0) {
return;
}
view.setBackgroundColor(color);
}
},
TEXT_COLOR_HINT("textColorHint") {
@Override
public void skin(View view, String resName) {
if (view == null){
return;
}
SkinResource skinResource = getSkinResource();
ColorStateList color = skinResource.getColorStateListByName(resName);
if (color == null) {
return;
}
TextView textView = (TextView) view;
textView.setHintTextColor(color);
}
},
SRC("src") {
@Override
public void skin(View view, String resName) {
if (view == null){
return;
}
SkinResource skinResource = getSkinResource();
Drawable drawable = skinResource.getDrawableByName(resName);
ImageView imageView = (ImageView) view;
if (drawable == null){
return;
}
imageView.setImageDrawable(drawable);
}
};
private String mResName; // 存储资源的名称
SkinType(String resName) { // 一个参数的构造方法
this.mResName = resName;
}
// 换肤的方法 具体实现由各种类型自己实现
public abstract void skin(View view, String resName);
public String getResName() {
return mResName;
}
public SkinResource getSkinResource() {
return SkinManager.getInstance().getSkinResource();
}
}
/**
* Created by hjcai on 2021/4/14.
*
* 存储了某个view的一个可以换肤的属性
* 存储的值为 当前资源名+属性类型
*/
public class SkinAttr {
private final String mResName;
private final SkinType mSkinType;
public SkinAttr(String resName, SkinType skinType) {
this.mResName = resName;
this.mSkinType = skinType;
}
// 对当前可以换肤的单独类型 进行换肤
public void skin(View view) {
mSkinType.skin(view,mResName);
}
}
/**
* Created by hjcai on 2021/4/14.
* 存储切换皮肤的一个view
* 该view可能存在多个属性需要换肤
*/
public class SkinView {
// 需要换肤的一个view
private final View mView;
// 需要换肤的属性用list保存 因为可能存在多个属性需要换肤
private final List<SkinAttr> mSkinAttrs;
public SkinView(View view, List<SkinAttr> skinAttrs) {
this.mView = view;
this.mSkinAttrs = skinAttrs;
}
// 换肤的方法
public void applySkin(){
// 遍历需要换肤view的中的可换肤属性们 进行换肤
for (SkinAttr attr : mSkinAttrs) {
attr.skin(mView);
}
}
}
/**
* Created by hjcai on 2021/4/14.
* <p>
* 皮肤管理类
* 换肤公开接口 调用SkinResource进行换肤
*/
public class SkinManager {
private static final SkinManager mInstance;
// 缓存 存储的内容是 activity与它里面需要换肤的views的键值对
private final Map<Activity, List<SkinView>> mAllSkinViewsInActivity = new HashMap<>();
// 皮肤实际操作者
private SkinResource mSkinResource;
// 为了避免内存泄漏 这里应该使用Application的context
private WeakReference<Context> contextWeakReference;
//静态代码块初始化
static {
mInstance = new SkinManager();
}
public static SkinManager getInstance() {
return mInstance;
}
// 根据activity获取需要换肤的所有view
public List<SkinView> getSkinViews(BaseSkinActivity activity) {
return mAllSkinViewsInActivity.get(activity);
}
// 缓存当前activity的所有换肤的view
public void cache(BaseSkinActivity activity, List<SkinView> skinViews) {
mAllSkinViewsInActivity.put(activity, skinViews);
}
// skinManager的初始化 在Application启动时初始化
public void init(Context context) {
contextWeakReference = new WeakReference<>(context);
}
// 加载任意皮肤
// 返回值表示是否成功换肤 后续会更新不同的返回值
public int loadSkin(String skinPath) {
// 初始化资源管理
mSkinResource = new SkinResource(contextWeakReference.get(), skinPath);
// 遍历存储的所有需要换肤的Activity
Set<Activity> keys = mAllSkinViewsInActivity.keySet();
for (Activity key : keys) {
List<SkinView> skinViewsInOneActivity = mAllSkinViewsInActivity.get(key);
// 更新所有Activity中的view
for (SkinView skinView : skinViewsInOneActivity) {
skinView.applySkin();
}
}
return 1;
}
// 恢复默认皮肤
public int restoreDefault() {
return 0;
}
// 获取当前皮肤资源
public SkinResource getSkinResource() {
return mSkinResource;
}
}
/**
* Created by hjcai on 2021/4/14.
* <p>
* 皮肤资源包管理类
*/
public class SkinResource {
// 通过加载不同path的Resources来加载不同皮肤包中的资源
private Resources mSkinResource;
private String mPackageName;
private static final String TAG = "SkinResource";
// 初始化构建mSkinResource对象
public SkinResource(Context context, String skinPath) {
try {
// 读取本地的一个 .skin里面的资源
Resources superRes = context.getResources();
// 创建AssetManager
AssetManager assetManager = AssetManager.class.newInstance();
// 寻找hide的addAssetPath方法
Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
// method.setAccessible(true); 如果是私有的
Log.e(TAG, "loadImg from : " + skinPath);
// 反射执行方法 assetManager指向指定的皮肤包 /storage/emulated/0/xxx.skin
method.invoke(assetManager, skinPath);
// 获取到加载资源的mSkinResource
mSkinResource = new Resources(assetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
// 获取指定路径apk的packageName
mPackageName = getPackageName(context, skinPath);
} catch (Exception e) {
Log.e(TAG, "loadImg: " + e.getStackTrace().toString());
e.printStackTrace();
}
}
private String getPackageName(Context context, String skinPath) {
if (context.getApplicationContext() != null) {
if (context.getApplicationContext().getPackageManager() != null) {
Log.e(TAG, "loadImage: myPath ===" + skinPath);
return context.getApplicationContext().getPackageManager().getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;
} else {
Log.e(TAG, "loadImage: getPackageManager==null");
return "";
}
}
return "";
}
public ColorStateList getColorStateListByName(String resName) {
try {
int resId = mSkinResource.getIdentifier(resName, "color", mPackageName);
ColorStateList color = mSkinResource.getColorStateList(resId);
return color;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 通过资源名字获取Drawable
*
* @param resName 资源名称
* @return Drawable
*/
public Drawable getDrawableByName(String resName) {
try {
int resId = mSkinResource.getIdentifier(resName, "drawable", mPackageName);
Log.e(TAG, "resId -> " + resId + " mPackageName -> " + mPackageName + " resName -> " + resName);
return mSkinResource.getDrawableForDensity(resId, 0, null);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public int getColorByName(String resName) {
try {
int resId = mSkinResource.getIdentifier(resName, "color", mPackageName);
return mSkinResource.getColor(resId);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
/**
* Created by hjcai on 2021/2/18.
*/
public abstract class BaseSkinActivity extends BaseActivity {
private static final String TAG = "BaseSkinActivity";
private SkinAppCompatViewInflater mAppCompatViewInflater;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
@Nullable
@Override
public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
//在这里拦截view创建
//可以在这里进行换肤
Log.e(TAG, "onCreateView: 拦截了view " + name);
// 换肤框架从这里开始搭建
// 1.创建View 目的是替换原先的view的一些属性
// createView走的代码流程和AppCompatDelegateImpl createView源码没有二致
// 即先走源码的流程 让源码帮我们创建好view 我们再检查这些view中的属性 看是否需要换肤
View view = createView(parent, name, context, attrs);
// 在拦截后与返回前进行所有可以进行换肤view的存储
// 2.解析属性 src textColor background textHintColor TODO 自定义属性先不考虑
if (view != null) {
// 获取当前activity中一个view中所有换肤的属性
List<SkinAttr> skinAttrs = SkinAttrSupport.getSkinAttrs(context, attrs);
// 创建skinView skinView中可能包含多个需要换肤的属性
SkinView skinView = new SkinView(view, skinAttrs);
// 3.交给SkinManager统一存储管理
managerSkinView(skinView);
}
return view;
}
@Nullable
@Override
public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {//Factory的方法 可以忽略
Log.e(TAG, "onCreateView: ");
return null;
}
});
super.onCreate(savedInstanceState);
}
// 在这里进行统一的皮肤存储
private void managerSkinView(SkinView skinView) {
List<SkinView> skinViews = SkinManager.getInstance().getSkinViews(this);
if (skinViews == null) {
// 之前没有存储过当前activity的view
skinViews = new ArrayList<>();
SkinManager.getInstance().cache(this, skinViews);
}
// 之前存储过当前activity的view
if (!skinViews.contains(skinView)){
skinViews.add(skinView);
}
}
// 拷贝自androidx.appcompat.app.AppCompatDelegateImpl 对view进行拦截处理
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
TypedArray a = context.obtainStyledAttributes(R.styleable.AppCompatTheme);
String viewInflaterClassName =
a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
if (viewInflaterClassName == null) {
// Set to null (the default in all AppCompat themes). Create the base inflater
// (no reflection)
mAppCompatViewInflater = new SkinAppCompatViewInflater();
} else {
try {
Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);
mAppCompatViewInflater =
(SkinAppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
Log.i(TAG, "Failed to instantiate custom view inflater "
+ viewInflaterClassName + ". Falling back to default.", t);
mAppCompatViewInflater = new SkinAppCompatViewInflater();
}
}
}
boolean inheritContext = false;
final boolean IS_PRE_LOLLIPOP = Build.VERSION.SDK_INT < 21;
if (IS_PRE_LOLLIPOP) {
inheritContext = (attrs instanceof XmlPullParser)
// If we have a XmlPullParser, we can detect where we are in the layout
? ((XmlPullParser) attrs).getDepth() > 1
// Otherwise we have to use the old heuristic
: shouldInheritContext((ViewParent) parent);
}
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
true/* 原本为VectorEnabledTintResources.shouldBeUsed() 先改为always true */
/* Only tint wrap the context if enabled */
);
}
// 拷贝自androidx.appcompat.app.AppCompatDelegateImpl
private boolean shouldInheritContext(ViewParent parent) {
if (parent == null) {
// The initial parent is null so just return false
return false;
}
final View windowDecor = this.getWindow().getDecorView();
while (true) {
if (parent == null) {
// Bingo. We've hit a view which has a null parent before being terminated from
// the loop. This is (most probably) because it's the root view in an inflation
// call, therefore we should inherit. This works as the inflated layout is only
// added to the hierarchy at the end of the inflate() call.
return true;
} else if (parent == windowDecor || !(parent instanceof View)
|| ViewCompat.isAttachedToWindow((View) parent)) {
// We have either hit the window's decor view, a parent which isn't a View
// (i.e. ViewRootImpl), or an attached view, so we know that the original parent
// is currently added to the view hierarchy. This means that it has not be
// inflated in the current inflate() call and we should not inherit the context.
return false;
}
parent = parent.getParent();
}
}
}
接着构建BaseSkinActivity的两个辅助类
/**
* This class is responsible for manually inflating our tinted widgets.
* <p>This class two main responsibilities: the first is to 'inject' our tinted views in place of
* the framework versions in layout inflation; the second is backport the {@code android:theme}
* functionality for any inflated widgets. This include theme inheritance from its parent.
*/
public class SkinAppCompatViewInflater {
private static final Class<?>[] sConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
private static final int[] sOnClickAttrs = new int[]{android.R.attr.onClick};
private static final String[] sClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
private static final String LOG_TAG = "AppCompatViewInflater";
private static final Map<String, Constructor<? extends View>> sConstructorMap
= new ArrayMap<>();
private final Object[] mConstructorArgs = new Object[2];
@SuppressLint("RestrictedApi")
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
case "RadioButton":
view = createRadioButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckedTextView":
view = createCheckedTextView(context, attrs);
verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
view = createAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
view = createMultiAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "RatingBar":
view = createRatingBar(context, attrs);
verifyNotNull(view, name);
break;
case "SeekBar":
view = createSeekBar(context, attrs);
verifyNotNull(view, name);
break;
default:
// The fallback that allows extending class to take over view inflation
// for other tags. Note that we don't check that the result is not-null.
// That allows the custom inflater path to fall back on the default one
// later in this method.
view = createView(context, name, attrs);
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
@NonNull
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
return new AppCompatTextView(context, attrs);
}
@NonNull
protected AppCompatImageView createImageView(Context context, AttributeSet attrs) {
return new AppCompatImageView(context, attrs);
}
@NonNull
protected AppCompatButton createButton(Context context, AttributeSet attrs) {
return new AppCompatButton(context, attrs);
}
@NonNull
protected AppCompatEditText createEditText(Context context, AttributeSet attrs) {
return new AppCompatEditText(context, attrs);
}
@NonNull
protected AppCompatSpinner createSpinner(Context context, AttributeSet attrs) {
return new AppCompatSpinner(context, attrs);
}
@NonNull
protected AppCompatImageButton createImageButton(Context context, AttributeSet attrs) {
return new AppCompatImageButton(context, attrs);
}
@NonNull
protected AppCompatCheckBox createCheckBox(Context context, AttributeSet attrs) {
return new AppCompatCheckBox(context, attrs);
}
@NonNull
protected AppCompatRadioButton createRadioButton(Context context, AttributeSet attrs) {
return new AppCompatRadioButton(context, attrs);
}
@NonNull
protected AppCompatCheckedTextView createCheckedTextView(Context context, AttributeSet attrs) {
return new AppCompatCheckedTextView(context, attrs);
}
@NonNull
protected AppCompatAutoCompleteTextView createAutoCompleteTextView(Context context,
AttributeSet attrs) {
return new AppCompatAutoCompleteTextView(context, attrs);
}
@NonNull
protected AppCompatMultiAutoCompleteTextView createMultiAutoCompleteTextView(Context context,
AttributeSet attrs) {
return new AppCompatMultiAutoCompleteTextView(context, attrs);
}
@NonNull
protected AppCompatRatingBar createRatingBar(Context context, AttributeSet attrs) {
return new AppCompatRatingBar(context, attrs);
}
@NonNull
protected AppCompatSeekBar createSeekBar(Context context, AttributeSet attrs) {
return new AppCompatSeekBar(context, attrs);
}
private void verifyNotNull(View view, String name) {
if (view == null) {
throw new IllegalStateException(this.getClass().getName()
+ " asked to inflate view for <" + name + ">, but returned null");
}
}
@Nullable
protected View createView(Context context, String name, AttributeSet attrs) {
return null;
}
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;
if (-1 == name.indexOf('.')) {
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {
return createViewByPrefix(context, name, null);
}
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
} finally {
// Don't retain references on context.
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
/**
* android:onClick doesn't handle views with a ContextWrapper context. This method
* backports new framework functionality to traverse the Context wrappers to find a
* suitable target.
*/
private void checkOnClickListener(View view, AttributeSet attrs) {
final Context context = view.getContext();
if (!(context instanceof ContextWrapper) ||
(Build.VERSION.SDK_INT >= 15 && !ViewCompat.hasOnClickListeners(view))) {
// Skip our compat functionality if: the Context isn't a ContextWrapper, or
// the view doesn't have an OnClickListener (we can only rely on this on API 15+ so
// always use our compat code on older devices)
return;
}
final TypedArray a = context.obtainStyledAttributes(attrs, sOnClickAttrs);
final String handlerName = a.getString(0);
if (handlerName != null) {
view.setOnClickListener(new DeclaredOnClickListener(view, handlerName));
}
a.recycle();
}
private View createViewByPrefix(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
Constructor<? extends View> constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
Class<? extends View> clazz = context.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
constructor = clazz.getConstructor(sConstructorSignature);
sConstructorMap.put(name, constructor);
}
constructor.setAccessible(true);
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
}
}
/**
* Allows us to emulate the {@code android:theme} attribute for devices before L.
*/
private static Context themifyContext(Context context, AttributeSet attrs,
boolean useAndroidTheme, boolean useAppTheme) {
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.View, 0, 0);
int themeId = 0;
if (useAndroidTheme) {
// First try reading android:theme if enabled
themeId = a.getResourceId(R.styleable.View_android_theme, 0);
}
if (useAppTheme && themeId == 0) {
// ...if that didn't work, try reading app:theme (for legacy reasons) if enabled
themeId = a.getResourceId(R.styleable.View_theme, 0);
if (themeId != 0) {
Log.i(LOG_TAG, "app:theme is now deprecated. "
+ "Please move to using android:theme instead.");
}
}
a.recycle();
if (themeId != 0 && (!(context instanceof ContextThemeWrapper)
|| ((ContextThemeWrapper) context).getThemeResId() != themeId)) {
// If the context isn't a ContextThemeWrapper, or it is but does not have
// the same theme as we need, wrap it in a new wrapper
context = new ContextThemeWrapper(context, themeId);
}
return context;
}
/**
* An implementation of OnClickListener that attempts to lazily load a
* named click handling method from a parent or ancestor context.
*/
private static class DeclaredOnClickListener implements View.OnClickListener {
private final View mHostView;
private final String mMethodName;
private Method mResolvedMethod;
private Context mResolvedContext;
public DeclaredOnClickListener(@NonNull View hostView, @NonNull String methodName) {
mHostView = hostView;
mMethodName = methodName;
}
@Override
public void onClick(@NonNull View v) {
if (mResolvedMethod == null) {
resolveMethod(mHostView.getContext(), mMethodName);
}
try {
mResolvedMethod.invoke(mResolvedContext, v);
} catch (IllegalAccessException e) {
throw new IllegalStateException(
"Could not execute non-public method for android:onClick", e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(
"Could not execute method for android:onClick", e);
}
}
@NonNull
private void resolveMethod(@Nullable Context context, @NonNull String name) {
while (context != null) {
try {
if (!context.isRestricted()) {
final Method method = context.getClass().getMethod(mMethodName, View.class);
if (method != null) {
mResolvedMethod = method;
mResolvedContext = context;
return;
}
}
} catch (NoSuchMethodException e) {
// Failed to find method, keep searching up the hierarchy.
}
if (context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
} else {
// Can't search up the hierarchy, null out and fail.
context = null;
}
}
final int id = mHostView.getId();
final String idText = id == View.NO_ID ? "" : " with id '"
+ mHostView.getContext().getResources().getResourceEntryName(id) + "'";
throw new IllegalStateException("Could not find method " + mMethodName
+ "(View) in a parent or ancestor Context for android:onClick "
+ "attribute defined on view " + mHostView.getClass() + idText);
}
}
}
/**
* Created by hjcai on 2021/4/14.
*/
public class SkinAttrSupport {
private static final String TAG = "SkinAttrSupport";
public static List<SkinAttr> getSkinAttrs(Context context, AttributeSet attrs) {
// 可能的属性 background src textColor textColorHint
// skinAttrs存储了当前一个view的所有可以换肤的属性
List<SkinAttr> skinAttrs = new ArrayList<>();
int attrLength = attrs.getAttributeCount();
for (int index = 0; index < attrLength; index++) {
String attrName = attrs.getAttributeName(index);
String attrValue = attrs.getAttributeValue(index);
//Log.e(TAG,"attrName -> "+attrName +" ; attrValue -> "+attrValue);
/*
<TextView
android:textColorHint="@color/black"
android:textColor="@color/half_black"
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
以上view的输出
2021-04-15 20:47:38.708 9628-9628/com.example.learneassyjoke E/SkinAttrSupport: attrName -> textColor ; attrValue -> @2131034442
2021-04-15 20:47:38.708 9628-9628/com.example.learneassyjoke E/SkinAttrSupport: attrName -> textColorHint ; attrValue -> @2131034263
2021-04-15 20:47:38.708 9628-9628/com.example.learneassyjoke E/SkinAttrSupport: attrName -> id ; attrValue -> @2131231140
2021-04-15 20:47:38.708 9628-9628/com.example.learneassyjoke E/SkinAttrSupport: attrName -> layout_width ; attrValue -> -2
2021-04-15 20:47:38.708 9628-9628/com.example.learneassyjoke E/SkinAttrSupport: attrName -> layout_height ; attrValue -> -2
2021-04-15 20:47:38.708 9628-9628/com.example.learneassyjoke E/SkinAttrSupport: attrName -> text ; attrValue -> Hello World!
<ImageView
android:background="@mipmap/ic_launcher"
android:src="@drawable/btn_back"
android:id="@+id/emptyImg"
android:layout_width="100dp"
android:layout_height="100dp" />
以上view的输出
2021-04-15 20:47:38.809 9628-9628/com.example.learneassyjoke E/SkinAttrSupport: attrName -> id ; attrValue -> @2131230879
2021-04-15 20:47:38.809 9628-9628/com.example.learneassyjoke E/SkinAttrSupport: attrName -> background ; attrValue -> @2131492864
2021-04-15 20:47:38.809 9628-9628/com.example.learneassyjoke E/SkinAttrSupport: attrName -> layout_width ; attrValue -> 100.0dip
2021-04-15 20:47:38.809 9628-9628/com.example.learneassyjoke E/SkinAttrSupport: attrName -> layout_height ; attrValue -> 100.0dip
2021-04-15 20:47:38.812 9628-9628/com.example.learneassyjoke E/SkinAttrSupport: attrName -> src ; attrValue -> @2131165279
补充:如果是定义的 #ccc这种写死的值 那么输出为
textColor ; attrValue -> #ffcccccc
这种情况是没有办法换肤的 我们会忽略这种情况
留意观察 我们需要的属性 background src textColor textColorHint 属性的值都是@后面跟一个int值 后面我们会用到这一特点
*/
// 只获取可能的属性 background src textColor textColorHint
SkinType skinType = getSkinType(attrName);
if (skinType != null){//不为null 就是我们需要换肤的部分
String resName = getResName(context,attrValue);
if(TextUtils.isEmpty(resName)){
continue;
}
// 创建一个属性
SkinAttr skinAttr = new SkinAttr(resName,skinType);
// 存储到集合中
skinAttrs.add(skinAttr);
}
}
return skinAttrs;
}
// 根据资源的id 找出资源的名称 我们希望的资源id应该是类似@2131230879 @2131492864这种的
private static String getResName(Context context, String attrValue) {
if(attrValue.startsWith("@")){//我们只关注开头是@的属性值 其他的我们也做不了换肤
//去掉开头的@
attrValue = attrValue.substring(1);
//转换成int的资源id
int resId = Integer.parseInt(attrValue);
//转换为资源名称 如btn_back color_black
if (resId <= 0){
return null;
}
return context.getResources().getResourceEntryName(resId);
}
return null;
}
// 通过名称获取SkinType
private static SkinType getSkinType(String attrName) {
SkinType[] skinTypes = SkinType.values();
// 遍历枚举 如果在枚举里面 则是我们想要的属性 否则返回null
for (SkinType skinType : skinTypes){
if(skinType.getResName().equals(attrName)){
// 返回值为 BACKGROUND SRC TEXT_COLOR TEXT_COLOR_HINT等我们定义好的类型
// Log.e(TAG, "getSkinType: "+skinType );
return skinType;
}
}
return null;
}
}
在换肤的activity调用换肤的方法
public void changeSkin(View view) {
SkinManager.getInstance().loadSkin(SkinPathUtil.getLightSkinPath());
}
辅助类SkinPathUtil
/**
* Created by hjcai on 2021/4/19.
*/
public class SkinPathUtil {
// return /storage/emulated/0/light.skin
public static String getLightSkinPath(){
return Environment.getExternalStorageDirectory().getAbsolutePath()
+ File.separator +"light.skin";
}
}
别忘了在Application中初始化skin Manager
SkinManager.getInstance().init(this);
最后就是准备皮肤包了
例如 我的xml布局如下:
main_activity
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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"
tools:context=".ActivityButterKnifeTest">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="请输入内容"
android:textColor="@color/text_color"
android:textColorHint="@color/text_hint" />
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textColor="@color/text_color" />
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/button_bg"
android:text="Hello World!"
android:textColor="@color/button_text" />
<Button
android:id="@+id/testOKHttp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/button_bg"
android:text="测试OKHttp"
android:textColor="@color/button_text" />
<Button
android:id="@+id/testMyOKHttp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/button_bg"
android:text="测试自己框架的OKHttp"
android:textColor="@color/button_text" />
<Button
android:id="@+id/clearData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/button_bg"
android:text="清除数据"
android:textColor="@color/button_text" />
<Button
android:id="@+id/queryAll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/button_bg"
android:text="查询所有"
android:textColor="@color/button_text" />
<Button
android:id="@+id/update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/button_bg"
android:text="更新"
android:textColor="@color/button_text" />
<Button
android:id="@+id/deleteByArgs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/button_bg"
android:text="删除指定"
android:textColor="@color/button_text" />
<ImageView
android:id="@+id/emptyImg"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@mipmap/ic_launcher"
android:src="@drawable/abc" />
<Button
android:id="@+id/testChangeSkin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/button_bg"
android:text="跳转换肤"
android:textColor="@color/button_text" />
</LinearLayout>
</ScrollView>
换肤activity:
<?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"
tools:context=".ActivityChangeSkin">
<Button
android:background="@color/button_bg"
android:textColor="@color/button_text"
android:id="@+id/changeSkin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="changeSkin"
android:text="换肤" />
<Button
android:background="@color/button_bg"
android:textColor="@color/button_text"
android:id="@+id/restoreSkin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="restoreSkin"
android:text="恢复默认皮肤" />
</LinearLayout>
为此我们需要准备两套皮肤
例如:
默认皮肤包
<color name="button_bg">#000</color>
<color name="button_text">#fff</color>
<color name="text_color">#f00</color>
<color name="text_hint">#ccc</color>
light主题的皮肤包
<color name="button_bg">#fff</color>
<color name="button_text">#000</color>
<color name="text_color">#800</color>
<color name="text_hint">#cf0</color>
当然drawable/abc中 abc.png 在两个apk中分别是两张图片
最后将生成的light皮肤包apk放到/storage/emulated/0/light.skin
3.最终效果
4.遗留问题
自定义view的换肤
如果切换过皮肤再进入应用不会保存皮肤
内存泄漏分析