前言
上篇文章介紹了Android-skin-support的缺陷(Android-skin-support的缺陷),那么這篇文章就來介紹一下如何解決這個(gè)缺陷。我們這里就通過一個(gè)自定義的換膚框架和Android-skin-support一起配合使用就可以滿足各種常規(guī)和非常規(guī)的換膚需求。CustomSkinChange是一款A(yù)ndroid自定義的換膚框架,功能強(qiáng)大,可以配合Android-skin-support一起使用,它通過反射換膚的skin包(其實(shí)就是APK包)獲取其中的資源動態(tài)的設(shè)置需要換膚的app。它配置有點(diǎn)麻煩,但是功能強(qiáng)大,它不僅可以完成Android-skin-support的功能,比如換控件的背景顏色、透明度、圖片,它還可以完成Android-skin-support不能完成的功能,比如坐標(biāo)、自定義屬性、字體內(nèi)容、控件的寬高等,它幾乎可以通過獲取包括皮膚包在Android項(xiàng)目資源文件下的任何資源進(jìn)行換膚。
這款框架還可以:
- 尤其對于某些自定義的控件的自定義屬性也可以切換皮膚包中的資源,比如圓的顏色以及圓的半徑都都可以根據(jù)換膚包切換。

image.png
- 字體的顏色、字體的內(nèi)容、字體的坐標(biāo)都難以通過換膚包切換

image.png
- 控件的寬高、坐標(biāo)位置也能根據(jù)皮膚包中的資源進(jìn)行切換

image.png
由于它需要通過反射來獲取皮膚包中的資源進(jìn)行動態(tài)換膚,所以需要換膚包提供反射的方法,配置也就會比Android-skin-support
麻煩,但是它可以完成Android-skin-support不能完成的需求,所以可以用Android-skin-support完成大部分可以完成
的換膚需求,然后通過CustomSkinChange去解決那些非常規(guī)的需求。
正文
先看看換膚的動畫效果

2017-12-20_20_30_05.gif
1.點(diǎn)擊按鈕切換皮膚包,并對換膚名稱進(jìn)行持久化
@Event(value = {R.id.skin_reset_tv, R.id.skin_red_tv, R.id.skin_color_tv})
private void onClick(View view) {
switch (view.getId()) {
case R.id.skin_reset_tv://默認(rèn)的藍(lán)色皮膚
Log.i( "skin_reset", "###重置" );
saveZhutiName( CustomViewSkinUtils.RESET_SKIN );
setSkinResource();
skinReFresh();
break;
case R.id.skin_red_tv://紅色皮膚
Log.i( "skin_reset", "###紅色" );
saveZhutiName( CustomViewSkinUtils.RED_SKIN );
CustomViewSkinUtils.copySkinApk( this, CustomViewSkinUtils.RED_SKIN );
setSkinResource();
skinReFresh();
break;
case R.id.skin_color_tv://多彩的圖片皮膚
saveZhutiName( CustomViewSkinUtils.COLOR_SKIN );
CustomViewSkinUtils.copySkinApk( this, CustomViewSkinUtils.COLOR_SKIN );
setSkinResource();
skinReFresh();
break;
}
}
利用sp對換膚的名稱進(jìn)行持久化
SpUtils.getInstance( this ).saveSkinName( highZhuTiName.substring( 0, len ) );
2.因?yàn)锳ndroid-skin-support的換膚包都是放在assets/skins的文件夾下面的,CustomSkinChange是配合Android-skin-support使用,那么它的換膚包也是從assets/skins文件夾下面取,取同樣的換膚包存入sd卡中(如果只想利用CustomSkinChange換膚,不用Android-skin-support框架,那么就可以把換膚包放在sd卡中)。
/**
* 因?yàn)锳ndroid-skin-support的皮膚包是放在assets中的,所以我們這里也從assets中獲取皮膚包
* 并保存在sdk中(這里只是為了兼容Android-skin-support ,其實(shí)可以直接把皮膚包放在sdk中的)
* @param context
* @param skinName 皮膚包的名稱(不包括后綴名)
*/
public static void copySkinApk(Context context, String skinName) {
try {
File dex = new File( Environment.getExternalStorageDirectory().toString() + File.separator + skinName + ".apk" );
InputStream input = context.getAssets().open( "skins/" + skinName + ".skin" );
dex.createNewFile();
FileOutputStream fo = new FileOutputStream( dex );
int a = 0;
byte bf[] = new byte[1024];
while ((a = input.read( bf )) != -1) {
fo.write( bf, 0, a );
fo.flush();
}
fo.close();
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
3.在調(diào)用setSkinResource()獲取換膚的資源
//設(shè)置換膚資源
public void setSkinResource() {
mSkinClzz = CustomViewSkinUtils.customViewChangeSkin( this );
mSkinResources = CustomViewSkinUtils.addOtherResourcesToMain( this );
}
/**
* 從sdk中獲取皮膚包,然后通過DexClassLoader獲取皮膚包中的設(shè)置色值、尺寸的類的Class
* @param context
* @return
*/
public static Class customViewChangeSkin(Context context) {
String skinName = SpUtils.getInstance( context ).getSkinName();
Log.i("skin_reset", "###"+skinName);
if (!DEFAULT_SKIN.equals( skinName )) {
try {
File dex_in = new File( Environment.getExternalStorageDirectory().toString() + File.separator + skinName + ".apk" );
String optimizedDirectory = context.getCacheDir() + File.separator;
Log.i("skin_reset", dex_in.getAbsolutePath()+"###"+optimizedDirectory+" "+dex_in.getName());
ClassLoader classLoader = new DexClassLoader( dex_in.getAbsolutePath(), optimizedDirectory, null, context.getClassLoader() );
Class clazz = classLoader.loadClass( "com.yw.skin.CustomGetResourcesUtils" );
return clazz;
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return null;
}
/**
* 通過addAssetPath將皮膚包中的資源增加到本apk自己的path里面以后 就可以重新構(gòu)建出resource對象了
* 利用這個(gè)Resource就可以通過反射獲取皮膚包中的資源了
* @param context
* @return
*/
public static Resources addOtherResourcesToMain(Context context) {
String skinName = SpUtils.getInstance( context ).getSkinName();
if (!DEFAULT_SKIN.equals( skinName )) {
File dex_in = new File( Environment.getExternalStorageDirectory().toString() + File.separator + skinName + ".apk" );
AssetManager assetManager = null;
try {
assetManager = AssetManager.class.newInstance();
//反射調(diào)用addAssetPath這個(gè)方法 就可以
Method addAssetPath = assetManager.getClass().getMethod( "addAssetPath", String.class );
addAssetPath.invoke( assetManager, dex_in.getAbsolutePath() );
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//把themeapk里的資源 通過addAssetPath 這個(gè)方法增加到本apk自己的path里面以后 就可以重新構(gòu)建出resource對象了
return new Resources( assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration() );
}
return null;
}
4.調(diào)用skinReFresh()進(jìn)行換膚
//開始換膚
public void skinReFresh() {
mSkinTextViewName = CustomViewSkinUtils.getCustomViewSkin( this, "skin_text_name", mSkinClzz, mSkinResources, mSkinTextViewNameCp );
mSkin_tv.setText( mSkinTextViewName );
mSkinTextViewMargin = CustomViewSkinUtils.getCustomViewSkin( this, "skin_text_magin", mSkinClzz, mSkinResources, mSkinTextViewMarginCp );
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) mSkin_tv.getLayoutParams();
layoutParams.leftMargin = (int) mSkinTextViewMargin;
mSkin_tv.setLayoutParams( layoutParams );
mSkinTextViewColor = CustomViewSkinUtils.getCustomViewSkin( this, "skin_text_color", mSkinClzz, mSkinResources, mSkinTextViewColorCp );
mSkin_tv.setTextColor( mSkinTextViewColor );
mSkinRlbg = CustomViewSkinUtils.getCustomViewSkin( this, "skin_bg_drawable", mSkinClzz, mSkinResources, mSkinRlbgCp );
mSkin_content_ll.setBackground( mSkinRlbg );
mSkinAllRlbg = CustomViewSkinUtils.getCustomViewSkin( this, "skin_bg_mipmap", mSkinClzz, mSkinResources, mSkinAllRlbgCp );
mSkin_all_rl.setBackground( mSkinAllRlbg );
mSkin_cricle.setSkinRefresh( mSkinClzz, mSkinResources );
}
5.調(diào)用CustomViewSkinUtils.getCustomViewSkin()獲取換膚包總的資源
如果換膚包不存在或者換膚包的內(nèi)容沒有此資源的反射方法,那么就返回默認(rèn)皮膚的資源。
public static <T> T getCustomViewSkin(Context context, String name, Class clazz, Resources resources, T normalParam) {
String skinName = SpUtils.getInstance( context ).getSkinName();
if ((clazz != null && resources != null) && !DEFAULT_SKIN.equals( skinName )) {
try {
Method method = clazz.getMethod( name, Resources.class );
T newSkinName = (T) method.invoke( null, resources );
return newSkinName;
} catch (Exception e) {
e.printStackTrace();
}
}
return normalParam;
}
6.對于自定義控件的自定義屬性的處理
radius和solid_color是自定義圓的半徑和顏色
<com.yw.skin.ywskin.wiget.CustomCircleView
android:id="@+id/skin_cricle"
android:layout_width="@dimen/skin_tv_circle_with"
android:layout_height="@dimen/skin_tv_circle_height"
android:layout_centerInParent="true"
app:radius="@integer/skin_tv_circle_radues"
app:solid_color="@color/reset_blist1_color"
/>
獲取自定義屬性,并且再用一個(gè)變量存儲這個(gè)默認(rèn)皮膚(本app)的資源
private void init(Context context, AttributeSet attrs) {
mContext = context;
TypedArray array = getContext().obtainStyledAttributes( attrs, R.styleable.Circle );
Circle_solid_color = array.getColor( R.styleable.Circle_solid_color, Color.BLUE );
Circle_radius = array.getInteger( R.styleable.Circle_radius, 50 ); //半徑
Circle_radiusCp = Circle_radius;//默認(rèn)皮膚的資源
Circle_solid_colorCp = Circle_solid_color;//默認(rèn)皮膚的資源
array.recycle();
mPaint.setDither( true ); //防抖
mPaint.setAntiAlias( true ); //抗鋸齒
mPaint.setStrokeWidth( 50 ); //設(shè)置畫筆的線寬
mPaint.setColor( Circle_solid_color );
}
開始換膚,如果相關(guān)的皮膚包不存在或者資源的反射方法也不存在,那么就返回默認(rèn)(本app)的資源
public void setSkinRefresh(Class clz, Resources res) {
Circle_solid_color = CustomViewSkinUtils.getCustomViewSkinColor( mContext, "Circle_solid_color", clz, res, Circle_solid_colorCp );
Circle_radius = CustomViewSkinUtils.getCustomViewSkinSize( mContext, "Circle_radius", clz, res, Circle_radiusCp );
mPaint.setColor( Circle_solid_color );
this.invalidate();//調(diào)用此方法進(jìn)行刷新布局
}
調(diào)用invalidate()就會重新執(zhí)行一次ondraw()方法,利用獲取的新資源重新畫圓
@Override
protected void onDraw(Canvas canvas) {
super.onDraw( canvas );
int sizeX = getWidth() / 2;
int sizeY = getHeight() / 2;
canvas.drawCircle( sizeX, sizeY, Circle_radius, mPaint );
canvas.save();
}