技术标签: Android
大家好,我是徐爱卿。博客地址:flutterall.com
这个SkinAPPDemo是很早的时候就写好的,今天才来总结,实在惭愧。–
其实,Android换肤这个功能呢从v7包中谷歌就跟我们做了一个很好的示范。同时呢,谷歌也给我们提供了一个针对View去做自定义操作的接口。说了这么多,不如来点实际的。
本篇博客的的demo中的build.gradle配置是:
compileSdkVersion 25
buildToolsVersion "25.0.0"
然后进行debug源码时,也是基于Android7.1.1的源码进行的。后面一大波debug来袭,请留神。
这篇文章我们会从最简单的XML文件开始聊起。个人觉得知识点有以下:
说白了,就是改变控件的背景以及颜色或者其本身的颜色。比如:更换TextView的字体颜色、背景颜色等等;在比如:更换LinearLayout的的背景以及颜色,等等。这些都属于换肤的范围。
###思考如何换肤?
两个切入点:
孰好孰坏,不言而喻。我呢,就要使用方法二。这个方法听起来不错,如何实现呢?其实,Google已经告诉我们了。天下文章一大抄,看你会抄不会抄。我们直接分析Google的实现逻辑,然后再写我们需要的逻辑。
有人问了,Android源码在哪里实现了?哥们别急,开讲了。
#引入
我们写一个简单的页面,里面就一个TextView,如下:
然后我们打印java Log.d(TAG, "tv instanceof AppCompatTextView ? -> "+(tv instanceof AppCompatTextView) +"");
这句话,如下:(注意红色框中的,就可以了)
看到结果不知道大家有没有些许疑问?在Android 7.1.1上运行的TextView竟然是AppCompatTextView的实例。我明明在XML中写的是TextView,在这里怎么就是AppCompatTextView的实例了呢?很明显,是在解析XML之后构建View对象的初期,看到是TextView标签直接使用AppCompatTextView构建这个对象。轮廓流程如下:
我们关键看最后一步,看下如何“创建AppCompatTextView”,当我们知道了如何偶从一个XML文件变身为一个View对象后,我们就可以比葫芦画瓢,创建我们的自己的属性的View了。
从这里开始,我们全部使用debug结果一部部分析并且来验证了,免得空口说大话了。
我这里,创建一个SelectThemeActivity,继承自AppCompatActivity 我们先从最简单的 setContentView(R.layout.activity_select_theme);
开始。
起初,执行的是
startActivity(new Intent(this, SelectThemeActivity.class));
],然后这个东东再向后调用ActivityManagerProxy#startActivity
, ActivityManagerProxy是ActivityManager的一个远程代理,不用管它。然后通过ActivityThread的内部Handler类执行performLaunchActivity,最后调用Instrumentation#callActivityOnCreate(Activity activity, Bundle icicle)
这一块的流程如下:
一定要记得下面这张图,这是一个关键点。
这里注意一点layoutInflater.getFactory()
,返回的是LayoutInflater的一个内部接口Factory
在这里默认没有代码干预的情况下,我们不设置Factory的情况下,layoutInflater.getFactory()等于null,系统会自己创建一个Factory去处理XML到View的转换。但是!!!他这个。反之,如果我们设置了自己的Factory,那么系统就会走我们Factory的onCreateView,他会返回一个我们定制化的View。下面详细讲解。
Factory定义如下:
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(String name, Context context, AttributeSet attrs);
}
Factory是一个很强大的接口。当我们使用inflating一个XML布局时,可以使用这个类进行拦截解析到的XML中的标签属性-AttributeSet和上下文-Context,以及标签名称-name(例如:TextView)。然后我们根据这些属性可以创建对应的View,设置一些对应的属性。
比如:我读取到XML中的TextView标签,这时,我就创建一个AppCompatTextView对象,它的构造方法中就是我读取到的XML属性。然后,将构造好的View返回即可。
默认情况下,从context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
得到LayoutInflater,然后通过layoutInflater.getFactory()
刚开始是null,然后执行LayoutInflaterCompat.setFactory(layoutInflater, this);
方法。
看下这个方法。
* Attach a custom Factory interface for creating views while using
* this LayoutInflater. This must not be null, and can only be set once;
* after setting, you can not change the factory.
*
* @see LayoutInflater#setFactory(android.view.LayoutInflater.Factory)
*/
public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
IMPL.setFactory(inflater, factory);
}
大致意识是:将一个自定义的Factory接口绑定到创建View的LayoutInflatr。这个接口的实现不能为空,同时只能设置一次(在代码中会有mFactorySet的boolean值(默认是false)标记是否已经设置过,如果重复设置,会抛异常)
在这里我们关注传入的LayoutInflaterFactory的实例,最终这个设置的LayoutInflaterFactory传入到哪里了呢?,我们向下debug,进入LayoutInflater中的下面:
给mFactory = mFactory2 = factory执行了,进行mFactory和mFactory2的赋值。
到这里走的路程,初始化好了LayoutInflater和LayoutInflaterFactory。
这里,我们就走完了SelectThemeActivity#onCreate中的super.onCreate(savedInstanceState);下面开始走
setContentView(R.layout.activity_select_theme);
setContentView会走到LayoutInflate的下面这里:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
//在这里将Resource得到layout的XmlResourceParser对象
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
再向下就到了LayoutInflate重点:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
.....
//将上面给我的XmlPullParser转换为对应的View的属性AttributeSet供View的构造方法或其他方法使用
final AttributeSet attrs = Xml.asAttributeSet(parser);
....
try {
if{
....
} else {
//默认布局会走到这里,Temp是XML文件的根布局
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
...
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
....
//添加解析到的根View
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
....
}
} catch (XmlPullParserException e) {
....
return result;
}
}
进入到createViewFromTag方法之中,会进入到LayoutInflate的View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr)
中。
到这里开始,我们开始学习源码中是如何使用Factory的。会走到下面这里:
这里的name传入的就是就是解析到的标签值LinearLayout。
@Override
public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// First let the Activity's Factory try and inflate the view
先试着进行解析布局
final View view = callActivityOnCreateView(parent, name, context, attrs);
if (view != null) {
return view;
}
// If the Factory didn't handle it, let our createView() method try
return createView(parent, name, context, attrs);
}
很遗憾, callActivityOnCreateView返回的总是null:
@Override
View callActivityOnCreateView(View parent, String name, Context context, AttributeSet attrs) {
// On Honeycomb+, Activity's private inflater factory will handle calling its
// onCreateView(...)
return null;
}
然后进入到下面的,createView(parent, name, context, attrs);中。高潮来了》》》》》,我期盼已久的看看Google源码是如何创建View的。
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 = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
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 it's android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
看到木有,它是拿标签名称进行switch的比较,是哪一个就进入到哪一个中进行创建View。
有人说啦,这里没有LinearLayout对应的switch啊。的确。最终返回null。
这里回到最初,由于Line769返回null,同时name值LinearLayout不包含".",进入到Line785onCreateView(parent, name, attrs)
。
到这里,我们知道这个标签是LinearLayout了,那么开始创建这个对象了。问题来了,我们知道这个对象名称了,但是它属于哪个包名?如何创建呢?
我们知道Android控件中的包名总共就那么几个:android.widget.]android.webkit.]android.app.]
,既然就这么几种,那么我干脆挨个用这些字符串进行如下拼接:
android.widget.LinearLayout]android.webkit.LinearLayout]android.app.LinearLayout],然后挨个创建对象,一旦创建成功即说明这个标签所在的包名是对的,返回这个对象即可。那么,从上面debug会进入到如下源码:
sClassPrefixList的定义如下:
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
注意:是final的
继续向下debug,进入到真正的创建Android布局标签对象的实现。在这个方法中,才是“android.widget.”包下的,LinearLayout、RelativeLayout等等的具体实现。
name=“LinearLayout”
prefix=“android.widget.”
下面分析下这段代码(下面的方法中去掉了一些无用代码):
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
//step1 :sConstructorMap是<标签名称:标签对象>的map,用来缓存对象的。第一次进入时,这个map中是空的。
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
//step2:在map缓存中没有找到对应的LinearLayout为key的对象,则创建。
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
//step3:【关键点,反射创建LinearLayout对象】,根据"prefix + name"值是"android.widget.LinearLayout"加载对应的字节码文件对象。
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
//step4:获取LinearLayout的Constructor对象
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
//step5:缓存LinearLayout的Constructor对象
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object[] args = mConstructorArgs;
args[1] = attrs;
//step6:args的两个值分别是SelectThemeActivity,XmlBlock$Parser。到这里就调用了LinearLayout的两个参数的构造方法去实例化对象。至此,LinearLayout的实现也就是Android中的布局文件的实现全部完成。最后把创建的View给return即可。
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
}
......
}
在这个方法中关键的步骤就是如何去实例化布局标签对象。这也是我们下面换肤的前提知识。
再让我们回到最初的地方:
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
//请注意下面?这个判断,系统肯定会走上面的mFactory2.onCreateView,
//默认系统的Factory返回的是null,
//所以系统会走下面自己的创建View的实现逻辑。
//如果我们在上面的流程图的第一步中设置了自己的Factory,那么系统
//会调用我们自己的Factory的createView的方法,这个时候,如果我们
//自己的Factory#onCreateView != null,那么就是返回我们的View了。
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
我们通过我们view的属性的值white,拿到skin-apk中的white属性的skinResId,然后根据skinRes.getColor(skinResId)返回color,然后设置到我们的TextView上面。
public class SkinActivity extends AppCompatActivity {
protected LayoutInflaterFactoryImpl layoutInflaterFactory;
@Override
protected void onCreate(Bundle savedInstanceState) {
layoutInflaterFactory = new LayoutInflaterFactoryImpl();
LayoutInflaterCompat.setFactory(getLayoutInflater(), layoutInflaterFactory);
super.onCreate(savedInstanceState);
}
}
看下运行结果:
OK!没问题,每一个View的实现我们都可以拦截到,下一步开始拿取View的background、或者TextColor进行相应的更改。
public class ViewAttrs {
public String attributeName, resourceEntryName, resourceTypeName;
public int resId;
public ViewAttrs(String attributeName, int resId, String resourceEntryName, String resourceTypeName) {
this.attributeName = attributeName;
this.resId = resId;
this.resourceEntryName = resourceEntryName;
this.resourceTypeName = resourceTypeName;
}
}
public class SkinView {
private View view;
private ArrayList<ViewAttrs> viewAttrses;
public SkinView(View view, ArrayList<ViewAttrs> viewAttrses) {
this.view = view;
this.viewAttrses = viewAttrses;
}
//android:textColor = "@color/red_color"
//android:background = "@mipmap/pic1"
//android:background = "@drawable/selector"
//android:background = "@color/blue_color"
public void changeTheme() {
//TODO 待实现的换肤代码
}
}
/**
* 解析本地view的属性,并保存该view
* 解析:view的属性名称;view的属性值;view的background;view的resId
* @param view
* @param context
* @param attrs
*/
private void saveViewAttrs(View view, Context context, AttributeSet attrs) {
//将view的每一种属性 以及对应的值放在list中
ArrayList<ViewAttrs> viewAttrses = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attributeName = attrs.getAttributeName(i);//background或者textColor
String attributeValue = attrs.getAttributeValue(i);//拿到view的id。类似于@2131361811
if(SkinConstans.BACKGROUND.equalsIgnoreCase(attributeName) || SkinConstans.TEXT_COLOR.equalsIgnoreCase(attributeName)){
//暂且这样判断,后面会有优化后的代码
int resId = Integer.parseInt(attributeValue.substring(1));//截取@2131361811 ,拿到实际的在R文件中的值
String resourceTypeName = context.getResources().getResourceTypeName(resId);//background的mipmap或者drawable或者color等
String resourceEntryName = context.getResources().getResourceEntryName(resId); //mipmap、drawable、color对应的值
ViewAttrs viewAttrs = new ViewAttrs(attributeName, resId, resourceEntryName, resourceTypeName);
viewAttrses.add(viewAttrs);
}
}
if(viewAttrses.size() > 0){
//保存需要换肤的view以及对应的属性
SkinView skinView = new SkinView(view, viewAttrses);
skinViews.add(skinView);
}
}
执行换肤时调用:
public void changeTheme(){
for (int i = 0; i < skinViews.size(); i++) {
skinViews.get(i).changeTheme();
}
}
public void loadSkin(String skinPath) {
//------------拿到skinPackageName----------
skinPackageName = context.getPackageManager().getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;
//----------拿到skin中的Resource对象----------
AssetManager assets = null;
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();
}
skinRes = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
/**
* @param resId
* @return
*/
public int getColor(int resId) {
if (skinRes == null) {
return resId;
}
//通过本地APP中的resId拿到本app对应的资源名称,然后再skin apk中找到该资源名称, 在根据skin中的资源名称 拿到对应的资源值
String resourceName = context.getResources().getResourceName(resId);
//String name, String defType, String skinPackageName 拿到skin包中的resId
int skinResId = skinRes.getIdentifier(resourceName.substring(resourceName.indexOf("/") + 1), SkinConstans.COLOR, skinPackageName);
if (skinResId == 0) {
//说明在skin皮肤中没有找到对应的resId,则返回原本的resId
return context.getResources().getColor(resId);
}
return skinRes.getColor(skinResId);
}
public Drawable getDrawable(int resId) {
Drawable drawable = context.getResources().getDrawable(resId);
if (skinRes == null) {
return drawable;
}
String resourceName = context.getResources().getResourceName(resId);
//String name, String defType, String skinPackageName 拿到skin包中的resId
int skinResId = skinRes.getIdentifier(resourceName.substring(resourceName.indexOf("/") + 1), SkinConstans.DRAWABLE, skinPackageName);
if (skinResId == 0) {
//说明在skin皮肤中没有找到对应的resId,则返回原本的resId
return drawable;
}
return skinRes.getDrawable(skinResId);
}
public void changeTheme() {
for (int i = 0; i < viewAttrses.size(); i++) {
ViewAttrs viewAttrs = viewAttrses.get(i);
if (SkinConstans.TEXT_COLOR.equalsIgnoreCase(viewAttrs.attributeName)) {
if (view instanceof TextView) {
//替换textColor
if (SkinConstans.COLOR.equalsIgnoreCase(viewAttrs.resourceTypeName)){
((TextView) view).setTextColor(SkinManager.getInstance().getColor(viewAttrs.resId));
}
}
} else if (SkinConstans.BACKGROUND.equalsIgnoreCase(viewAttrs.attributeName)) {
if (SkinConstans.DRAWABLE.equalsIgnoreCase(viewAttrs.resourceTypeName)) {
view.setBackgroundDrawable(SkinManager.getInstance().getDrawable(viewAttrs.resId));
} else if (SkinConstans.COLOR.equalsIgnoreCase(viewAttrs.resourceTypeName)) {
view.setBackgroundColor(SkinManager.getInstance().getColor(viewAttrs.resId));
} else if (SkinConstans.MIPMAP.equalsIgnoreCase(viewAttrs.resourceTypeName)) {
}
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/activity_main_color"
android:orientation="vertical"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="xu.myapplication.MainActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/text_background_color"
android:text="Hello World!"
android:textColor="@color/text_color" />
<Button
android:text="换肤"
android:onClick="changeTheme"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
color.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="text_color">#aadd00</color>
<color name="text_background_color">#3F51B5</color>
<color name="activity_main_color">#009977</color>
</resources>
MainActivity
public class MainActivity extends SkinActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void changeTheme(View view){
SkinManager.getInstance().initContext(this);
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
SkinManager.getInstance().loadSkin(Environment.getExternalStorageDirectory().getAbsolutePath()+"/skinplugin-debug.apk");
layoutInflaterFactory.changeTheme();
}
}
下面是skin-apk的color.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#3F51B5</color>
<color name="text_color">#FF4081</color>
<color name="text_background_color">#1199aa</color>
<color name="activity_main_color">#aadd00</color>
</resources>
Activity的background和TextView的textColor都换了
优化这一块,有很多地方可以优化。比如:
private void saveViewAttrs(View view, Context context, AttributeSet attrs) {
//将view的每一种属性 以及对应的值放在list中
ArrayList<ViewAttrs> viewAttrses = new ArrayList<>();
boolean skinEnable = true;
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attributeName = attrs.getAttributeName(i);//background或者textColor
String attributeValue = attrs.getAttributeValue(i);//拿到view的在R文件中的id。类似于@2131361811
/*if(SkinConstant.BACKGROUND.equalsIgnoreCase(attributeName) || SkinConstant.TEXT_COLOR.equalsIgnoreCase(attributeName)){
int resId = Integer.parseInt(attributeValue.substring(1));//截取@2131361811 ,拿到实际的在R文件中的值
String resourceTypeName = context.getResources().getResourceTypeName(resId);//background的mipmap或者drawable或者color等
String resourceEntryName = context.getResources().getResourceEntryName(resId); //mipmap、drawable、color对应的值
ViewAttrs viewAttrs = new ViewAttrs(attributeName, resId, resourceEntryName, resourceTypeName);
viewAttrses.add(viewAttrs);
}*/
if("skin".equalsIgnoreCase(attributeName)){
//默认对所有控件换肤,但是如果属性中包含有[skin:skin=""],则表示不对该控件做换肤处理
skinEnable = false;
break;
}
if(!ViewAttrsFactory.contains(attributeName) || attributeValue.indexOf("@") < 0){
continue;
}
int resId = Integer.parseInt(attributeValue.substring(1));//截取@2131361811 ,拿到实际的在R文件中的值
String resourceTypeName = context.getResources().getResourceTypeName(resId);//background的mipmap或者drawable或者color等
String resourceEntryName = context.getResources().getResourceEntryName(resId); //mipmap、drawable、color对应的值
ViewAttrs viewAttrs = ViewAttrsFactory.createViewAttrs(attributeName, resId, resourceEntryName, resourceTypeName);
if (viewAttrs != null) {
viewAttrses.add(viewAttrs);
}
}
if (skinEnable && viewAttrses.size() > 0) {
//保存需要换肤的view以及对应的属性
SkinView skinView = new SkinView(view, viewAttrses);
skinViews.add(skinView);
if(SkinManager.getInstance().isLoadSkinSuccess()){
skinView.changeTheme();
}
}
}
ViewAttrsFactory
public class ViewAttrsFactory {
public static Map<String, ViewAttrs> viewAttrsMap = new HashMap<>();
static {
//添加支持换肤的属性
viewAttrsMap.put(SkinConstant.TEXT_COLOR, new TextColorViewAttrs());
viewAttrsMap.put(SkinConstant.BACKGROUND, new BackgroundViewAttrs());
viewAttrsMap.put(SkinConstant.SRC, new BackgroundViewAttrs());
viewAttrsMap.put(SkinConstant.MENU, new NavigationMenuAttrs());
}
public static ViewAttrs createViewAttrs(String attributeName, int resId, String resourceEntryName, String resourceTypeName) {
if (viewAttrsMap.get(attributeName) != null) {
ViewAttrs viewAttrs;
if ((viewAttrs = viewAttrsMap.get(attributeName).clone()) != null) {
viewAttrs.attributeName = attributeName;
viewAttrs.resId = resId;
viewAttrs.resourceEntryName = resourceEntryName;
viewAttrs.resourceTypeName = resourceTypeName;
return viewAttrs;
}
}
return null;
}
public static boolean contains(String attributeName) {
return attributeName != null && viewAttrsMap.get(attributeName) != null;
}
}
更多优化,在我的GitHub上SkinAppDemo,大家拉下来看下。这里就不在赘述了。
多多debug,多多益善!上面的我贴的debug的流程,还希望大家多多debug。反正我不记得我debug这个流程多少遍了。
谢谢大家最后坚持看到这里,期望大家多多fork,多多start。谢谢大家。
文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib
文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang
文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些
文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器
文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距
文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器
文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn
文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios
文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql
文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...
文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120
文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数