Android应用内换肤
换肤简介
换肤本质
上是对资源的一种替换包括、字体、颜色、背景、图片、大小等等。比如View的修改背景颜色setBackgroundColor,TextView的setTextSize修改字体等等。
换肤方案
目前Android换肤有两种类型,静态换肤
和动态换肤
;静态换肤就是将所有的皮肤方案放到项目中,而动态换肤则就是从网络加载皮肤包动态切换;
- 通常静态换肤是通过Theme实现,通过在项目中定义多套主题,使用setTheme方法切换的方式实现换肤;
- 动态换肤是通过替换系统的Resouce动态加载下载到本地的资源包实现换肤。
下面我们对这个两种种换肤方式进行讲解。
静态换肤之Theme换肤
这种方式是谷歌官方推荐的方式,很多google的app都是使用的这种方式,据说知乎也是使用的这种方式,这种方式的优点就是使用很简单,首先在res/values/attrs.xml下定义属性名称和类型
<?xml version="1.0" encoding="utf-8"?>
<resources><attr name="main_bg" format="reference|color"/><attr name="main_textcolor" format="reference|color"/><attr name="main_textAppearance" format="reference|color"/><attr name="main_btn_bg" format="reference|color"/><attr name="second_bg" format="reference|color"/><attr name="second_textcolor" format="reference|color"/>
</resources>
接着准备一些资源,这里以color资源举例,在res/values/colors.xml中定义一些color:
<?xml version="1.0" encoding="utf-8"?>
<resources><color name="bg_main_normal">#ffffcc</color><color name="textcolor_main_normal">#ff0000</color><color name="bg_main_dark">#000000</color><color name="textcolor_main_dark">#33ffff</color><color name="bg_second_normal">#0000ff</color><color name="textcolor_second_normal">#00ff00</color><color name="bg_second_dark">#ffffff</color><color name="textcolor_second_dark">#000000</color><color name="main_textAppearance">#000000</color><color name="drak_textAppearance">#ffffff</color>
</resources>
然后在定义两套主题,分别引用不同的颜色资源:
<?xml version="1.0" encoding="utf-8"?>
<resources><style name="theme_1" ><item name="main_bg">@color/bg_main_normal</item><item name="main_textcolor">@color/textcolor_main_normal</item><item name="main_btn_bg">@color/textcolor_main_dark</item><item name="main_textAppearance">@style/theme_1_btn</item><item name="second_bg">@color/bg_second_normal</item><item name="second_textcolor">@color/textcolor_second_normal</item></style><style name="theme_2"><item name="main_bg">@color/bg_main_dark</item><item name="main_textcolor">@color/textcolor_main_dark</item><item name="main_btn_bg">@color/textcolor_main_normal</item><item name="main_textAppearance">@style/theme_2_btn</item><item name="second_bg">@color/bg_second_dark</item><item name="second_textcolor">@color/textcolor_second_dark</item></style></resources>
接着在布局文件中通过下面的方式引用资源文件
android:background="?attr/main_bg"
最后在Activity的setContentView方法前设置想要的主题就可以了,注意,设置主题一定要在setContentView方法前,否则不起作用。
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// setTheme方法要在setContentView之前调用if(SharedPreferencesMgr.getInt("theme", 0) == 1) {setTheme(R.style.theme_2);} else {setTheme(R.style.theme_1);}setContentView(R.layout.activity_main2);}
这里有几个问题:
1.当我们在应用中切换主题时只有重新创建的Activity才会使用新的主题,所以这里就需要我们手动调用一下recreate()
方法,或者重启应用,代码如下
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
// 杀掉当前进程,这行代码也只能杀当前进程,杀不了其他进程
android.os.Process.killProcess(android.os.Process.myPid());
// 退出Java虚拟机,进程也就退出了,好像不加也可以
System.exit(0);
这样就可以重新创建页面切换主题,但是这样做有两个弊端,一是屏幕会出现一下闪烁,二是页面重新创建之后要考虑数据的恢复。这个问题也是这个方案最大的弊端。
我们可以不重新创建Activity而是在切换主题后手动修改已创建Activity的View的颜色等信息,但是我们不能在每个页面都写上修改页面View信息的方法,这样的工作量太大了,我们把切换主题的页面入口放到最底层,也就是说如果你想切换主题必须回到主页面,这样我们只需要在主页面添加这样的方法就可以了,这也算一个取巧的方法。然后为每个需要换肤的View进行封装,这样做会很麻烦,代码如下:
public class ColorTextView extends TextView implements ColorUiInterface {private int attr_drawable = -1;private int attr_textAppearance = -1;private int attr_textColor = -1;// // 在代码中直接new一个Custom View实例的时候,会调用一个参数构造函数.这个没有任何争议. 在xml布局文件中调用Custom View的时候,会调用二个参数构造函数.这个也没有争议. 在xml布局文件中调用Custom View,并且Custom View标签中还有自定义属性时,这里调用的还是二个参数构造函数. 也就是说,系统默认只会调用Custom View的前两个构造函数,至于第三个构造函数的调用,通常是我们自己在构造函数中主动调用的(例如,在第二个构造函数中调用第三个构造函数).public ColorTextView(Context context) {super(context);}// AttributeSet attrs: 属性值的集合.// int[] attrs: 我们自定义属性集合在R类中生成的int型数组.这个数组中包含了自定义属性的资源ID.// int defStyleAttr: 这是当前Theme中的包含的一个指向style的引用.当我们没有给自定义View设置declare-styleable资源集合时,默认从这个集合里面查找布局文件中配置属性值.传入0表示不向该defStyleAttr中查找默认值.// int defStyleRes: 这个也是一个指向Style的资源ID,但是仅在defStyleAttr为0或者defStyleAttr不为0但Theme中没有为defStyleAttr属性赋值时起作用.// 在布局xml中直接定义 > 在布局xml中通过style定义 > 自定义View所在的Activity的Theme中指定style引用 > 构造函数中defStyleRes指定的默认值public ColorTextView(Context context, AttributeSet attrs) {super(context, attrs);this.attr_textAppearance = ViewAttributeUtil.getTextApperanceAttribute(attrs);this.attr_drawable = ViewAttributeUtil.getBackgroundAttibute(attrs);this.attr_textColor = ViewAttributeUtil.getTextColorAttribute(attrs);//<derson.com.multipletheme.colorUi.widget.ColorTextView// android:textColor="?attr/main_textcolor"// android:layout_width="wrap_content"// android:layout_height="wrap_content"// android:text="@string/hello_world" />// ColorTextView2: attr_textAppearance -1 attr_drawable -1 attr_textColor 2130771971Log.d("ruanyandong", "ColorTextView2: attr_textAppearance "+attr_textAppearance+" attr_drawable "+attr_drawable+" attr_textColor "+attr_textColor);}public ColorTextView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);this.attr_textAppearance = ViewAttributeUtil.getTextApperanceAttribute(attrs);this.attr_drawable = ViewAttributeUtil.getBackgroundAttibute(attrs);this.attr_textColor = ViewAttributeUtil.getTextColorAttribute(attrs);}@Overridepublic View getView() {return this;}@Overridepublic void setTheme(Resources.Theme themeId) {if (attr_drawable != -1) {ViewAttributeUtil.applyBackgroundDrawable(this, themeId, attr_drawable);}if(attr_textAppearance != -1) {ViewAttributeUtil.applyTextAppearance(this, themeId, attr_textAppearance);}if (attr_textColor != -1) {ViewAttributeUtil.applyTextColor(this, themeId, attr_textColor);}}
}
public class ViewAttributeUtil {public static int getAttributeValue(AttributeSet attr, int paramInt) {int value = -1;int count = attr.getAttributeCount();//getAttributeValue: attr.getAttributeCount() 7 paramInt 16842904Log.d("ruanyandong", "getAttributeValue: attr.getAttributeCount() "+attr.getAttributeCount()+" paramInt "+paramInt);for(int i = 0; i <count;i++) {if(attr.getAttributeNameResource(i) == paramInt) {//str ?2130771968String str = attr.getAttributeValue(i);//getAttributeValue: attr.getAttributeName(i) background attr.getAttributeValue(i) ?2130771968//getAttributeValue: attr.getAttributeName(i) textColor attr.getAttributeValue(i) ?2130771971Log.d("ruanyandong", "getAttributeValue: attr.getAttributeName(i) "+attr.getAttributeName(i)+" attr.getAttributeValue(i) "+attr.getAttributeValue(i));if(null != str && str.startsWith("?")) {value = Integer.parseInt(str.substring(1));return value;}}}return value;}public static int getBackgroundAttibute(AttributeSet attr) {return getAttributeValue(attr , android.R.attr.background);}public static int getCheckMarkAttribute(AttributeSet attr) {return getAttributeValue(attr, android.R.attr.checkMark);}public static int getSrcAttribute(AttributeSet attr) {return getAttributeValue(attr, android.R.attr.src);}public static int getTextApperanceAttribute(AttributeSet attr) {return getAttributeValue(attr, android.R.attr.textAppearance);}public static int getDividerAttribute(AttributeSet attr) {return getAttributeValue(attr, android.R.attr.divider);}public static int getTextColorAttribute(AttributeSet attr) {return getAttributeValue(attr, android.R.attr.textColor);}public static void applyBackgroundDrawable(ColorUiInterface ci, Resources.Theme theme, int paramInt) {TypedArray ta = theme.obtainStyledAttributes(new int[]{paramInt});Drawable drawable = ta.getDrawable(0);if(null != ci) {(ci.getView()).setBackgroundDrawable(drawable);}ta.recycle();}public static void applyImageDrawable(ColorUiInterface ci, Resources.Theme theme, int paramInt) {TypedArray ta = theme.obtainStyledAttributes(new int[]{paramInt});Drawable drawable = ta.getDrawable(0);if(null != ci && ci instanceof ImageView) {((ImageView)ci.getView()).setImageDrawable(drawable);}ta.recycle();}public static void applyTextAppearance(ColorUiInterface ci, Resources.Theme theme, int paramInt) {TypedArray ta = theme.obtainStyledAttributes(new int[]{paramInt});int resourceId = ta.getResourceId(0,0);if(null != ci && ci instanceof TextView) {((TextView)ci.getView()).setTextAppearance(ci.getView().getContext(), resourceId);}ta.recycle();}public static void applyTextColor(ColorUiInterface ci, Resources.Theme theme, int paramInt) {TypedArray ta = theme.obtainStyledAttributes(new int[]{paramInt});int resourceId = ta.getColor(0,0);if(null != ci && ci instanceof TextView) {((TextView)ci.getView()).setTextColor(resourceId);}ta.recycle();}}
2.如果一个老项目想用这个方案就很难受了,因为除了要定义多套资源外,我们还要把布局文件中的资源引用全部修改一遍,这样做会很麻烦。
代码具体参考 dersoncheng /MultipleTheme 或者 stven0king /MultipleTheme 或者Android-dynamic-change-skin/MultipleTheme
动态换肤之Resource换肤(插件是换肤)
动态换肤的一般步骤为:
- 制作皮肤包
- 下载并加载皮肤包,拿到皮肤包Resource对象
- 标记需要换肤的View
- 缓存需要换肤的View
- 切换时即时刷新页面
1、制作皮肤包
制作皮肤包比较简单,在Android Studio中创建一个Application模块,删除里面的Java代码,把需要的资源文件放到res对应的文件夹里
然后build项目,点击Build->Make Project,生成皮肤包apk文件,名字就叫skin_plugin.apk,然后放到sd卡根目录下面,路径就是
Environment.getExternalStorageDirectory() + File.separator + "skin_plugin.apk"
到这里皮肤包就制作好了。
2、下载并加载皮肤包,拿到皮肤包Resource对象
因为我们已经把皮肤包放到sd卡的根目录了,所以就不需要下载了,我们直接加载皮肤插件apk包。
private void loadPlugin(String skinPath, String skinPkgName, String suffix) throws Exception {//checkPluginParams(skinPath, skinPkgName);AssetManager assetManager = AssetManager.class.newInstance();Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);addAssetPath.invoke(assetManager, skinPath);Resources superRes = mContext.getResources();mResources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());mResourceManager = new ResourceManager(mResources, skinPkgName, suffix);usePlugin = true;}
skinPath
是插件包在sd卡的路径, skinPkgName
是插件包的包名, suffix
是插件资源的后缀。
构造一个AssetManager
对象并调用addAssetPath方法设置资源文件的路径,由于addAssetPath方法是hide注解的,我们不能直接调用,所以我们通过反射来调用这个方法,最后使用我们构造的AssetManager对象和原来的DisplayMetrics、Configuration构造一个Resources对象。
拿到Resoures对象通过下面的方法获取需要的资源:
getIdentifier(String name, String defType, String defPackage)
第一个参数是资源的名称,比如R.color.red,其中red就是name
第二个参数是资源类型,比如R.String.appname,其中String就是类型
第三个参数是资源所在的包名,这是打皮肤包设置的,一般是自己应用的包名
下面是资源获取的类
public class ResourceManager {/*** 资源类型*/private static final String DEFTYPE_DRAWABLE = "drawable";private static final String DEFTYPE_COLOR = "color";private final Resources mResources;/*** 插件包名*/private final String mPluginPackageName;/*** 资源后缀*/private final String mSuffix;public ResourceManager(Resources res, String pluginPackageName, String suffix) {mResources = res;mPluginPackageName = pluginPackageName;if (suffix == null) {suffix = "";}mSuffix = suffix;}/*** 获取drawable资源* @param name* @return*/public Drawable getDrawableByName(String name) {try {name = appendSuffix(name);XLog.e("name = " + name);return mResources.getDrawable(mResources.getIdentifier(name, DEFTYPE_DRAWABLE, mPluginPackageName));} catch (Resources.NotFoundException e) {try {return mResources.getDrawable(mResources.getIdentifier(name, DEFTYPE_COLOR, mPluginPackageName));} catch (Resources.NotFoundException e2) {e.printStackTrace();return null;}}}/*** 获取color资源* @param name* @return*/public int getColor(String name) {try {name = appendSuffix(name);XLog.e("name = " + name);return mResources.getColor(mResources.getIdentifier(name, DEFTYPE_COLOR, mPluginPackageName));} catch (Resources.NotFoundException e) {e.printStackTrace();return -1;}}/*** 获取colorSelect* @param name* @return*/public ColorStateList getColorStateList(String name) {try {name = appendSuffix(name);XLog.e("name = " + name);return mResources.getColorStateList(mResources.getIdentifier(name, DEFTYPE_COLOR, mPluginPackageName));} catch (Resources.NotFoundException e) {e.printStackTrace();return null;}}/*** 拼接资源名称后缀* @param name* @return*/private String appendSuffix(String name) {if (!TextUtils.isEmpty(mSuffix))return name + ("_" + mSuffix);return name;}}
3、标记需要换肤的View
这里有两种办法。
1. 第一种
就是在布局文件里给直接给需要换肤的view设置tag属性,tag属性以skin:
开头,后面紧跟资源名称
和属性名称
,例如:skin:left_menu_icon:src
,
对于一个View多个属性需要换肤的,android:tag="skin:item_text_color:textColor|skin:icon:src"
同样使用|进行分隔,如果是动态添加的view直接使用setTag
方法进行标记。
<ImageViewandroid:id="@+id/id_iv_icon"android:layout_width="42dp"android:layout_height="42dp"android:layout_centerVertical="true"android:layout_marginRight="8dp"android:tag="skin:left_menu_icon:src"android:src="@drawable/left_menu_icon"/>
2. 还有一种方式
在布局文件中自定义一个属性,例如:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"xmlns:skin="http://schemas.android.com/android/skin"android:layout_width="match_parent"android:layout_height="match_parent"skin:enable="true" android:background="@color/color_app_bg" ><TextViewandroid:id="@+id/detail_text"android:layout_width="wrap_content"android:layout_height="wrap_content"skin:enable="true" /></RelativeLayout>
导入自定义命名空间xmlns:skin="http://schemas.android.com/android/skin"
,需要换肤的view添加skin:enable=“true”
我们自定义的属性,名字什么的都无所谓,true就是需要换肤,false就是不需要换肤。
4、缓存需要换肤的View
- 第一种
如果是以tag的形式进行标记,则缓存View的方式就是找到每个页面最外层且id为android.R.id.content
的VIewGroup,遍历所有view,代码如下:
public class SkinAttrSupport {/*** 判断是否支持该属性换肤** @param attrName* @return*/private static SkinAttrType getSupportAttrType(String attrName) {for (SkinAttrType attrType : SkinAttrType.values()) {if (attrType.getAttrType().equals(attrName))return attrType;}return null;}/*** 传入activity,找到content元素,递归遍历所有的子View,根据tag命名,记录需要换肤的View** @param activity*/public static List<SkinView> getSkinViews(Activity activity) {List<SkinView> skinViews = new ArrayList<SkinView>();ViewGroup content = (ViewGroup) activity.findViewById(android.R.id.content);addSkinViews(content, skinViews);return skinViews;}/*** 根据当前Activity的id为android.R.id.content的控件,递归的遍历所有View,获取对应的换肤属性* @param view* @param skinViews*/public static void addSkinViews(View view, List<SkinView> skinViews) {SkinView skinView = getSkinView(view);if (skinView != null) skinViews.add(skinView);if (view instanceof ViewGroup) {ViewGroup container = (ViewGroup) view;for (int i = 0, n = container.getChildCount(); i < n; i++) {View child = container.getChildAt(i);addSkinViews(child, skinViews);}}}/*** 根据当前View的tag获取换肤属性和对应的资源名称,然后根据View和换肤属性列表构造SkinView* @param view* @return*/public static SkinView getSkinView(View view) {Object tag = view.getTag(R.id.skin_tag_id);if (tag == null) {tag = view.getTag();}if (tag == null) return null;if (!(tag instanceof String)) return null;String tagStr = (String) tag;List<SkinAttr> skinAttrs = parseTag(tagStr);if (!skinAttrs.isEmpty()) {changeViewTag(view);return new SkinView(view, skinAttrs);}return null;}/*** 给当前View设置tag* @param view*/private static void changeViewTag(View view) {Object tag = view.getTag(R.id.skin_tag_id);if (tag == null) {tag = view.getTag();XLog.e("view.getTag() == "+tag);view.setTag(R.id.skin_tag_id, tag);view.setTag(null);}}/*** 根据tag进行解析,获取所有支持换肤的属性放在list中,每个皮肤属性包含 view的属性类型 和 资源名称* @param tagStr* @return*///skin:left_menu_icon:src|skin:color_red:textColorprivate static List<SkinAttr> parseTag(String tagStr) {List<SkinAttr> skinAttrs = new ArrayList<SkinAttr>();if (TextUtils.isEmpty(tagStr)) return skinAttrs;String[] items = tagStr.split("[|]");for (String item : items) {if (!item.startsWith(SkinConfig.SKIN_PREFIX))continue;String[] resItems = item.split(":");if (resItems.length != 3)continue;String resName = resItems[1];String resType = resItems[2];SkinAttrType attrType = getSupportAttrType(resType);if (attrType == null) continue;SkinAttr attr = new SkinAttr(attrType, resName);skinAttrs.add(attr);}return skinAttrs;}}
最终所有需要换肤的view缓存在List<SkinView>
里面,这种方式手机和缓存view没有什么入侵性。
- 第二种
如果是以skin:enable=“true”
方式进行标记的话,则收集和缓存的view的方式就是继承LayoutInflater.Factory2
或者LayoutInflater.Factory
,重写onCreateView
方法,把这个Fractory设置给LayoutInflater,代替系统创建view的过程,在此过程中拿到所有需要换肤的view,代码如下:
public class SkinInflaterFactory implements Factory2 {private static final boolean DEBUG = true;/*** Store the view item that need skin changing in the activity*/private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {// if this is NOT enable to be skined , simplly skip it boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);if (!isSkinEnable){return null;}View view = createView(context, name, attrs);if (view == null){return null;}parseSkinAttr(context, attrs, view);return view;}/*** Invoke low-level function for instantiating a view by name. This attempts to* instantiate a view class of the given <var>name</var> found in this* LayoutInflater's ClassLoader.* * @param context * @param name The full name of the class to be instantiated.* @param attrs The XML attributes supplied for this instance.* * @return View The newly instantiated view, or null.*/private View createView(Context context, String name, AttributeSet attrs) {View view = null;try {if (-1 == name.indexOf('.')){if ("View".equals(name)) {view = LayoutInflater.from(context).createView(name, "android.view.", attrs);} if (view == null) {view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);} if (view == null) {view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);} }else {view = LayoutInflater.from(context).createView(name, null, attrs);}L.i("about to create " + name);} catch (Exception e) { L.e("error while create 【" + name + "】 : " + e.getMessage());view = null;}return view;}/*** Collect skin able tag such as background , textColor and so on* * @param context* @param attrs* @param view*/private void parseSkinAttr(Context context, AttributeSet attrs, View view) {List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();for (int i = 0; i < attrs.getAttributeCount(); i++){String attrName = attrs.getAttributeName(i);String attrValue = attrs.getAttributeValue(i);if(!AttrFactory.isSupportedAttr(attrName)){continue;}if(attrValue.startsWith("@")){try {int id = Integer.parseInt(attrValue.substring(1));String entryName = context.getResources().getResourceEntryName(id);String typeName = context.getResources().getResourceTypeName(id);SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);if (mSkinAttr != null) {viewAttrs.add(mSkinAttr);}} catch (NumberFormatException e) {e.printStackTrace();} catch (NotFoundException e) {e.printStackTrace();}}}if(!ListUtils.isEmpty(viewAttrs)){SkinItem skinItem = new SkinItem();skinItem.view = view;skinItem.attrs = viewAttrs;mSkinItems.add(skinItem);if(SkinManager.getInstance().isExternalSkin()){skinItem.apply();}}}public void applySkin(){if(ListUtils.isEmpty(mSkinItems)){return;}for(SkinItem si : mSkinItems){if(si.view == null){continue;}si.apply();}}public void dynamicAddSkinEnableView(Context context, View view, List<DynamicAttr> pDAttrs){ List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();SkinItem skinItem = new SkinItem();skinItem.view = view;for(DynamicAttr dAttr : pDAttrs){int id = dAttr.refResId;String entryName = context.getResources().getResourceEntryName(id);String typeName = context.getResources().getResourceTypeName(id);SkinAttr mSkinAttr = AttrFactory.get(dAttr.attrName, id, entryName, typeName);viewAttrs.add(mSkinAttr);}skinItem.attrs = viewAttrs;addSkinView(skinItem);}public void dynamicAddSkinEnableView(Context context, View view, String attrName, int attrValueResId){ int id = attrValueResId;String entryName = context.getResources().getResourceEntryName(id);String typeName = context.getResources().getResourceTypeName(id);SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);SkinItem skinItem = new SkinItem();skinItem.view = view;List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();viewAttrs.add(mSkinAttr);skinItem.attrs = viewAttrs;addSkinView(skinItem);}public void addSkinView(SkinItem item){mSkinItems.add(item);}}
把所有view收集在List<SkinItem> mSkinItems
中,同时在对应的BaseActivity中把这个Fractory设置LayoutInflater:
@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {LayoutInflater layoutInflater = LayoutInflater.from(this);LayoutInflaterCompat.setFactory2(layoutInflater, new SkinInflaterFactory());super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}
为什么可以这样写呢?因为在super.onCreate(savedInstanceState);
这行代码的内部源码处,会去判断是否设置过Factory2,没有设置则会去创建并设置,然后在setContentView(R.layout.activity_main);
内部会去解析布局XML文件,调用已经设置给LayoutInflater
的Factory2
的onCreateView创建对应的View,如果我们在系统设置Factory2
给LayoutInflater
之前就把我们自定义的Fractory设置给了LayoutInflater
,那么系统就不会再设置Factory了,我们便代替系统创建了view。
这样做入侵性比较强,如果系统内部有更新改动,我们的方案很有可能就不行了。
5. 切换时即时刷新页面
- tag方式标记,换肤刷新页面
/*** 对当前Activity进行换肤* @param activity*/public void apply(Activity activity) {List<SkinView> skinViews = SkinAttrSupport.getSkinViews(activity);if (skinViews == null) return;for (SkinView skinView : skinViews) {skinView.apply();}}/*** 对当前Activity进行换肤* @param activity*/public void register(final Activity activity) {mActivities.add(activity);activity.findViewById(android.R.id.content).post(new Runnable() {@Overridepublic void run() {apply(activity);}});}/*** 移除当前换肤的Activity* @param activity*/public void unregister(Activity activity) {mActivities.remove(activity);}/*** 对所有注册了的Activity进行焕肤*/public void notifyChangedListeners() {for (Activity activity : mActivities) {apply(activity);}}/*** 对当前View和其子View进行焕肤** @param view*/public void injectSkin(View view) {List<SkinView> skinViews = new ArrayList<SkinView>();SkinAttrSupport.addSkinViews(view, skinViews);for (SkinView skinView : skinViews) {skinView.apply();}}
- skin:enable方式标记换肤刷新页面
这里贴出部分代码,具体请参考文后的demo链接。
public void applySkin(){if(ListUtils.isEmpty(mSkinItems)){return;}for(SkinItem si : mSkinItems){if(si.view == null){continue;}si.apply();}}public void apply(){if(ListUtils.isEmpty(attrs)){return;}for(SkinAttr at : attrs){at.apply(view);}}public class BackgroundAttr extends SkinAttr {@Overridepublic void apply(View view) {if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId));}else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);view.setBackground(bg);}}
}
动态换肤到这里就讲完了,下面附上了代码链接,有兴趣可自行参阅。
代码具体参考InvasionChangeSkin或者NoInvasionChangeSkin或者Android-Skin-Loader
参考文章
Android换肤总结
Android换肤原理和Android-Skin-Loader框架解析
Android 切换主题以及换肤的实现
Android应用内换肤相关推荐
- Android插件化换肤
Android插件化换肤 前言(废话) 今年是大年三十,今年怎么说呢,总体还是让自己感觉到比较满意的,但是有些时候还是感觉自己的自觉性不够.先贤曾经说过,君子慎独,愿明年的我能够铭记于心. 我这辈子最 ...
- Android应用程序换肤实现系列(一)
转载请标明出处:http://blog.csdn.net/EdisonChang/article/details/50021467 国内已经有很多android应用软件支持个性化皮肤定制功能,比如QQ ...
- Android模拟器的换肤和Android学习资料下载
现如今,说到Android,不知者,可以说是寥寥无几,就连邻家小妹,也在玩Android.Android的火爆,足以看到移动市场可见一斑啊.移动市场的巨大空间,巨大商机,很多人都在蠢蠢欲动 ...
- Android App节日换肤
Android App节日换肤 Android App节日换肤 1原理 2使用方式 1在XML中给需要换肤的控件添加tag属性 2在Activity中使用 3还有疑问吧 3示例图 比如支付宝,饿了么, ...
- Android美女一键换肤
今天看到有同事在群里聊到仿QQQ 皮肤切换主题,然后我就试着写一个简单的Demo希望大家能够支持下! 像以前扣扣换肤 废话不多说上效果 思路如下 通过应用程序内置资源实现换肤,典型的应用为QQ空间中换 ...
- Android 几种换肤方式和原理分析
1.通过Theme切换主题 通过在setContentView之前设置Theme实现主题切换. 在styles.xml定义一个夜间主题和白天主题: <style name="Light ...
- Android APK方式换肤实现原理
现在很多APP都有换肤的功能,例如微博,QQ等应用.这些应用的换肤原理是什么? 在用微博的时候,不难发现,当你要换肤时,先下载并安装一个皮肤apk,然后选择这个皮肤,就可以了. 这种方式就是把皮肤打包 ...
- Android 主题切换/换肤方案 研究(四) - qq和qq空间
4. qq和qq空间 (独立app) 分析时用的是: 1. 夜神android模拟器(因为用android studio自带的模拟器运行x86架构的镜像提示不能安装qq空间,安装arm架构的镜像运行又 ...
- android插件式换肤核心实现
本文思路来源于腾讯课堂,在此记录与大家分享并记录后用 在setContentView之前对view进行拦截 @Overrideprotected void onCreate(Bundle savedI ...
最新文章
- 类中的关键字public、protected、private究竟是什么意思?
- 关于坐标系,关于矩阵及线性相关和无关的关系
- Stars HDU 1541
- 第二阶段--个人冲刺--第十天
- Sklearn.metrics评估方法
- 在PHP中利用wsdl创建标准webservice
- 【程序设计】程序错误类型
- 管理工作中的50点感悟
- golang中的匿名组合
- Fiddler实现IOS手机抓取https报文
- C#使用Advanced CSharp Messenger
- 用 Python 爬取了 14 年的福彩 3D 信息!彩民们,只能帮你们到这了
- 12306数据库遭泄露,请尽快修改密码
- java基础语法(三)--运算符、控制语句
- 优动漫PAINT的变形文字工具教程
- 24C02 EEPROM多个字节连续写入乱码问题解决
- CVE-2014-6321 MS14-066 Microsoft Schannel Remote Code Execution Vulnerability Analysis
- 【LabVIEW串口编程】03 串口接收
- 【博弈论】第二讲:纳什均衡的混合战略(有限数量战略)
- centos7分区挂载大容量数据盘
热门文章
- 计算机网络与新媒体是什么,网络与新媒体专业课程是什么
- github self-hosted runner 添加与启动
- 九宫格拼图怎么拼?分享两个简单的操作
- TiDB+TiSpark部署--安装,扩缩容及升级操作
- NOIP历年第二轮入门组真题集合
- iphone原彩显示对眼睛好吗_iPhone12又拉胯?用户吐槽屏幕发黄,到底是为啥?
- 网贷害人,迷途知返后,天真的以为外包只要会增删改查就够了???
- 设置成GPU后仍然在使用CPU跑程序
- Kubernetes容器云平台技术方案
- git拉取代码报错fatal Authentication failed for ‘httpxxxx.git‘‘解决方案