前言:
上一篇文章我们储备了一些基础知识,现在要开始着手筛选出需要换肤的 View。在上篇文章中说过,需要分两步,先获取所有的 View,再进行筛选。
上一篇文章地址:https://www.jianshu.com/p/ec0704524528
获取所有 View
先创建上篇文章中提到的自定义工厂类。
/**
* 自定义 Factory2
*/
public class SkinLayoutFactory implements LayoutInflater.Factory2 {
/**
* 一般 Android 系统的 View 都存储在这几个包下面
*/
private static final String[] mClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
/**
* 系统调用的是两个参数的构造方法,我们也调用这个构造方法
*/
private static final Class>[] mConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
private static final String SPOT = ".";
/**
* 创建 View 的过程会回调到该 onCreateView 的方法中,并且每有一个 View 就调用一次
*/
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (TextUtils.isEmpty(name)) {
return null;
}
/*
我们模仿源码那样来创建 View
*/
View view = createViewFromTag(name, context, attrs);
/*
这里如果 View 返回的是 null 的话,就是自定义控件,
自定义控件不需要我们进行拼接,可以直接拿到全类名
*/
if (view == null) {
view = createView(name,context,attrs);
}
return view;
}
/**
* 真正创建 View 的方法
*/
private View createView(String name, Context context, AttributeSet attributeSet) {
try {
//通过反射来获取 View 实例对象
Class extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
Constructor extends View> constructor = aClass.getConstructor(mConstructorSignature);
if (constructor != null) {
return constructor.newInstance(aClass, attributeSet);
} else {
throw new Exception("该 View 没有指定的构造函数");
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
/**
* 创建系统自带的 View
*
* @param name View 的名字,比如 ImageView,Button,EditText
* @param context 上下文
* @param attributeSet 属性
* @return View
*/
private View createViewFromTag(String name, Context context, AttributeSet attributeSet) {
//如果 name 中包含了".",暂时不做处理,返回 null
if (SPOT.equals(name)) {
return null;
}
View view = null;
//拼接 name
for (int i = 0; i < mClassPrefixList.length; i++) {
view = createView(mClassPrefixList[i]+name,context,attributeSet);
if(view != null){
break;
}
}
return view;
}
}
获取所有的 View 已经完成,但是还是有些问题,看 createViewFromTag() 方法,加入,我们的布局文件中有两个 ImageView,两个 Button,三个 TextView,那是不是意味着我们这几个 View 需要反射创建对象 7 次呢?按照我们的代码来说,答案是肯定的,这样不好,我们使用缓存,来提高一下效率。
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义 Factory2
*/
public class SkinLayoutFactory implements LayoutInflater.Factory2 {
/**
* 用于缓存
*/
private static final Map> sConstructorMap = new HashMap<>();
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//...
}
/**
* 真正创建 View 的方法
*/
private View createView(String name, Context context, AttributeSet attributeSet) {
Constructor extends View> constructor = sConstructorMap.get(name);
//通过反射来获取 View 实例对象
if(constructor == null){
try {
Class extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
constructor = aClass.getConstructor(mConstructorSignature);
sConstructorMap.put(name,constructor);
} catch (Exception e) {
e.printStackTrace();
}
}
if(constructor != null){
try {
return constructor.newInstance(context,attributeSet);
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
//...
}
至此,所有的 View 已经获取完毕,接下来,我们需要对 View 进行筛选。
筛选 View
需要换肤的 View 肯定是设置了一些可换肤属性,我们需要根据获取的 View ,拿到它在 xml 中设置的属性,然后根据属性来判断是否要进行换肤。
新建 SkinAttribute 类,来进行筛选。
import android.content.res.ColorStateList;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.radish.android.skin_core.util.SkinResources;
import com.radish.android.skin_core.util.SkinThemeUtils;
import java.util.ArrayList;
import java.util.List;
/**
* 筛选需要进行换肤的 View
*/
public class SkinAttribute {
private static final List mAttributes = new ArrayList<>();
private List skinViews = new ArrayList<>();
/**
* 如果 View 设置了如下的属性,
* 我们还需要进行下一步的判断,才能知道该 View 是否需要换肤
*/
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
}
/**
* 对 View 进行筛选
*
* @param view 被筛选的 View
* @param attributeSet 被筛选的 View 对应的 attributeSet
*/
public void filtrate(View view, AttributeSet attributeSet) {
List skinPairs = new ArrayList<>();
for (int i = 0; i < attributeSet.getAttributeCount(); i++) {
String attributeName = attributeSet.getAttributeName(i);
if (mAttributes.contains(attributeName)) {
//该 View 属性中包含了需要换肤的属性
String attributeValue = attributeSet.getAttributeValue(i);
/*
这里,假如获取的值是这样的 android:textColor="#ffffff"
写死了,那么我们不管
*/
if (attributeValue.startsWith("#")) {
continue;
}
int resId;
/*
android:background="?attr/colorAccent"
那么我们需要去 style 中再次获取 resId
*/
if (attributeValue.startsWith("?")) {
int attrId = Integer.valueOf(attributeValue.substring(1));
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
} else {
/*
可以直接获取 resId
输出 attributeValue 的值是这样的:@开头的资源 attributeValue = @2130837604
*/
resId = Integer.valueOf(attributeValue.substring(1));
}
if (resId != 0) {
/*
这里保存的是我们需要换肤的属性--资源 id 这个映射关系,然后将这些映射关系保存到 List 里面
*/
SkinPair skinPair = new SkinPair(attributeName, resId);
skinPairs.add(skinPair);
}
}
}
/*
这里保存以后,我们的对应关系是 view -- 属性表,其中属性表对应的关系是 属性名 -- 资源 id
*/
if (!skinPairs.isEmpty()) {
SkinView skinView = new SkinView(view, skinPairs);
skinView.applySkin();
skinViews.add(skinView);
}
}
/**
* 提供外部调用换肤的方法
*/
public void applySkin() {
for(int i = 0 ;i < skinViews.size();i++){
SkinView skinView = skinViews.get(i);
skinView.applySkin();
}
}
private static class SkinView {
View view;
List skinPairs;
public SkinView(View view, List skinPairs) {
this.view = view;
this.skinPairs = skinPairs;
}
/**
* 换肤操作
*/
public void applySkin() {
for (SkinPair skinPair : skinPairs) {
Drawable left = null,top = null,right = null,bottom = null;
switch (skinPair.attributeName) {
case "background":
Object background = SkinResources.getInstance().getBackground(skinPair.resId);
if (background instanceof Integer) {
view.setBackgroundColor((Integer) background);
} else {
ViewCompat.setBackground(view, (Drawable) background);
}
break;
case "textColor":
ColorStateList colorStateList = SkinResources.getInstance().getColorStateList(skinPair.resId);
((TextView) view).setTextColor(colorStateList);
break;
case "src":
Object bg = SkinResources.getInstance().getBackground(skinPair.resId);
if(bg instanceof Integer){
((ImageView) view).setImageDrawable(new ColorDrawable((Integer) bg));
}else{
((ImageView) view).setImageDrawable((Drawable) bg);
}
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableRight":
right = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableBottom":
bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
default:
break;
}
if(left != null || top != null || right != null || bottom != null){
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left,top,right,bottom);
}
}
}
}
private static class SkinPair {
String attributeName;
int resId;
SkinPair(String attributeName, int resId) {
this.attributeName = attributeName;
this.resId = resId;
}
}
}
import android.content.Context;
import android.content.res.TypedArray;
/**
* 工具类
*/
public class SkinThemeUtils {
/**
* 无法直接从 AttributeSet 中获取到资源 id 的情况下,需要通过转换的方式来进行获取
* 比如说,android:background="?attr/colorAccent"
* 这里 ? 后面拿到值后,还需要去 style.xml 文件中继续获取
* 对应资源 id,在 style.xml 文件中拿到的才是资源 id
* @param context 上下文
* @param attrs 需要获取的资源 id
* @return 资源 id
*/
public static int[] getResId(Context context, int[] attrs) {
int[] resId = new int[attrs.length];
TypedArray typedArray = context.obtainStyledAttributes(attrs);
for (int i = 0; i < typedArray.length(); i++) {
resId[i] = typedArray.getResourceId(i,0);
}
typedArray.recycle();
return resId;
}
}
然后千万不要忘了调用 skinAttribute 的 filtrate() 方法。
/**
* 自定义 Factory2
*/
public class SkinLayoutFactory implements LayoutInflater.Factory2 {
private SkinAttribute skinAttribute;
public SkinLayoutFactory(SkinAttribute skinAttribute){
this.skinAttribute = skinAttribute;
}
//...
/**
* 创建 View 的过程会回调到该 onCreateView 的方法中,并且每有一个 View 就调用一次
*/
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//...
if(skinAttribute != null){
skinAttribute.filtrate(view,attrs);
}
return view;
}
//...
}
写了这么多代码,先测试一下吧。改一下 MainActivity 的布局文件:
然后在 MainActivity 中加上这句话
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LayoutInflater layoutInflater = getLayoutInflater();
try {
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater,false);
} catch (Exception e) {
e.printStackTrace();
}
layoutInflater.setFactory2(new SkinLayoutFactory(new SkinAttribute()));
setContentView(R.layout.activity_main);
}
具体这句话的作用后面会说,然后我们在将前面保存 View 的集合输出:
private void testSkinPairs(List skinPairs) {
for (int i = 0; i < skinPairs.size(); i ++) {
SkinPair skinPair = skinPairs.get(i);
Log.i(TAG, "abc : skinPair.View = " + skinPair.view.getClass().getSimpleName()");
}
}
看 MainActivity 的布局文件,LinearLayout 有 background 属性,并且值是 ?attr/colorAccent,需要换肤,进行保存;第一个 TextView 虽然有 background,但值是写死的,不保存;第二个 TextView 不但有 background,还有 textColor,而且值都是可以换肤的,保存;而 Button 没有任何要换肤的属性,不保存。那么结果就是一个 LinearLayout 和 一个 TextView
abc : skinPair.View = LinearLayout
abc : skinPair.View = TextView
没问题。
下一篇文章地址:https://www.jianshu.com/p/1139df041cb6