静态换肤是在APP应用内部放置多套资源,进行资源的切换,静态换肤一般用在APP的日夜间模式切换中。
一.原理
当在Activity中使用setContentView加载布局时,会调用到AppCompatDelegateImpl的setContentView方法,该方法代码如下:
@Override
public void setContentView ( int resId) {
. . .
View view = tryCreateView ( parent, name, context, attrs) ;
if ( view == null) {
final Object lastContext = mConstructorArgs[ 0 ] ;
mConstructorArgs[ 0 ] = context;
try {
if ( - 1 == name. indexOf ( '.' ) ) {
view = onCreateView ( context, parent, name, attrs) ;
} else {
view = createView ( context, name, null, attrs) ;
}
} finally {
mConstructorArgs[ 0 ] = lastContext;
}
}
return view;
. . .
}
以上代码中,通过tryCreateView方法进行控件的创建,tryCreateView的代码如下:
public final View tryCreateView ( @Nullable View parent, @NonNull String name,
. . .
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) ;
}
return view;
}
在tryCreateView方法中,首先通过mFactory2的onCreateView方法创建View对象,mFactory2的类型是Factory2,Factory2是一个接口,只有一个onCreateView方法,Activity就实现了这个接口。mFactory2初始化过程在AppComPatActivity类的onCreate方法中,通过delegate.installViewFactory()方法进行初始化,该方法如下:
@Override
public void installViewFactory ( ) {
LayoutInflater layoutInflater = LayoutInflater. from ( mContext) ;
if ( layoutInflater. getFactory ( ) == null) {
LayoutInflaterCompat. setFactory2 ( layoutInflater, this ) ;
} else {
if ( ! ( layoutInflater. getFactory2 ( ) instanceof AppCompatDelegateImpl ) ) {
Log. i ( TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's" ) ;
}
}
}
只有当layoutInflater.getFactory()为空时,会采用AppCompatDelegateImpl作为Factory2对象,然后后续执行AppCompatDelegateImpl的createView方法创建View对象。
基于以上流程,静态换肤可以采取以下方式:
在代码执行delegate.installViewFactory()方法之前,将Factory2对象设置成我们自己写的Factory2,这样通过createView进行View对象的创建可以按照我们的思路进行。只需在super.onCreate方法执行之前调用LayoutInflaterCompat.setFactory2方法设置我们自己的Factory2对象。 在创建View对象时,拿到View的background、textColor等与换肤相关的属性,在换肤时通过改变属性值进行换肤。
二.静态换肤实现:
1.SkinActivity
首先新建一个SkinActivity作为App中一切Activity的基类,SkinActivity继承自AppCompatActivity。其onCreate方法如下:
@Override
protected void onCreate ( @Nullable Bundle savedInstanceState) {
LayoutInflaterCompat. setFactory2 ( LayoutInflater. from ( this ) , this ) ;
super . onCreate ( savedInstanceState) ;
}
由于Activity都实现了Factory2接口,所以可以重写SkinActivity的onCreateView方法,该方法如下:
@Override
public View onCreateView ( @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
if ( openSkin ( ) ) {
if ( null == viewInflater) {
viewInflater = new MyAppCompatViewInflater ( context) ;
}
viewInflater. setName ( name) ;
viewInflater. setAttrs ( attrs) ;
return viewInflater. createView ( ) ;
}
return super . onCreateView ( name, context, attrs) ;
}
public View createView ( ) {
View resultView = null;
switch ( name) {
case "TextView" :
resultView = new MyTextView ( context, attrs) ;
break ;
case "ImageView" :
resultView = new MyImageView ( context, attrs) ;
break ;
case "Button" :
resultView = new MyButton ( context, attrs) ;
break ;
case "LinearLayout" :
resultView = new MyLinearLayout ( context, attrs) ;
break ;
case "RelativeLayout" :
resultView = new MyRelativeLayout ( context, attrs) ;
break ;
}
return resultView;
}
2.自定义View
在布局加载时,需要使用自定义的View去代替Android中的View,在自定义View之前,首先新建一个ViewChange接口,该接口包含一个换肤方法skinChange,我们自定义的View需要实现该接口用来执行换肤操作。
以自定义Button为例,首先在/res/valuse/attrs.xml下把Button控件换肤时需要变换的属性列出来,如下所示:
< resources>
< declare- styleable name= "MyButton" >
< attr name= "android:background" / >
< attr name= "android:textColor" / >
< attr name= "android:text" / >
< / declare- styleable>
< / resources>
新建一个类MyButton,该类继承Button并且实现ViewChange接口。在MyButton的构造方法中,对换肤时需要变化的三个属性及其对应的ResourceId使用sparseIntArray进行保存。
public MyButton ( Context context, AttributeSet attrs, int defStyleAttr) {
super ( context, attrs, defStyleAttr) ;
attrsBean = new AttrsBean ( ) ;
TypedArray typedArray = context. obtainStyledAttributes ( attrs, R. styleable. MyButton, defStyleAttr, 0 ) ; attrsBean. saveViewResource ( typedArray, R. styleable. MyButton) ;
typedArray. recycle ( ) ;
}
public void saveViewResource ( TypedArray typedArray, int [ ] styleable) {
for ( int i= 0 ; i < typedArray. length ( ) ; i++ ) {
int key = styleable[ i] ;
int resourceId = typedArray. getResourceId ( i, DEFAULT_VALUE) ;
sparseIntArray. put ( key, resourceId) ;
}
}
在重写的ViewChange接口的换肤方法skinChange中,拿到保存在sparseIntArray中的属性id和相应的ResourceId,通过ResouceId得到资源并给控件属性设置值。
@Override
public void skinChange ( ) {
int key = R. styleable. MyButton[ R. styleable. MyButton_android_background] ;
int backgroundId = attrsBean. getViewResource ( key) ;
if ( backgroundId > 0 ) {
Drawable drawable = ContextCompat. getDrawable ( getContext ( ) , backgroundId) ;
setBackgroundDrawable ( drawable) ;
}
key = R. styleable. MyButton[ R. styleable. MyButton_android_textColor] ;
int textColorId = attrsBean. getViewResource ( key) ;
if ( textColorId > 0 ) {
ColorStateList colorStateList = ContextCompat. getColorStateList ( getContext ( ) , textColorId) ;
setTextColor ( colorStateList) ;
}
key = R. styleable. MyButton[ R. styleable. MyButton_android_text] ;
int textId = attrsBean. getViewResource ( key) ;
if ( textId > 0 ) {
String str = getContext ( ) . getString ( textId) ;
setText ( str) ;
}
}
3.App中的Activity
在写App中的xml布局时,需要准备日间和夜间模式两套资源,夜间模式的资源放在drawable-night和values-night中,Android在Configuration类中提供了两个常量UI_MODE_NIGHT_NO和UI_MODE_NIGHT_YES表示日间和夜间模式,当处于夜间模式时,布局会加载drawable-night和values-night中的资源。
对于App中的Activity,需要继承SkinActivity,当进行换肤操作时,执行如下方法:
public void dayOrNight ( View view) {
int uiMode = getResources ( ) . getConfiguration ( ) . uiMode & Configuration. UI_MODE_NIGHT_MASK;
switch ( uiMode) {
case Configuration. UI_MODE_NIGHT_NO:
setDayNightMode ( AppCompatDelegate. MODE_NIGHT_YES) ;
break ;
case Configuration. UI_MODE_NIGHT_YES:
setDayNightMode ( AppCompatDelegate. MODE_NIGHT_NO) ;
break ;
}
}
其中setDayNightMode方法写在SkinActivity中。
public void setDayNightMode ( int mode) {
getDelegate ( ) . setLocalNightMode ( mode) ;
View decorView = getWindow ( ) . getDecorView ( ) ;
changeSkin ( decorView) ;
}
private void changeSkin ( View decorView) {
if ( decorView instanceof ViewChange ) {
ViewChange viewChange = ( ViewChange) decorView;
viewChange. skinChange ( ) ;
}
if ( decorView instanceof ViewGroup ) {
ViewGroup viewGroup = ( ViewGroup) decorView;
for ( int i= 0 ; i< viewGroup. getChildCount ( ) ; i++ ) {
changeSkin ( viewGroup. getChildAt ( i) ) ;
}
}
}
换肤效果如图所示: