换肤方案原理在网上已经很多了, 这里不再详细描述, 强迫症的我总是想让提供给别人使用的SDK尽量好用, 哪怕是给自己带来额外的工作量, 经过一段时间的奋斗, 实现了一个自我感觉良好的换肤框架.
这里主要来看看Android 源码中”com.android.support:appcompat-v7”包的实现, 以及源码思想在Android-skin-support中的应用 – 如何打造一款好用的换肤框架.
首先来看一下源码的实现:
AppCompatActivity源码
public class AppCompatActivity extends FragmentActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
...
}
@Override
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
....
}
AppCompatActivity 将大部分生命周期委托给了AppCompatDelegate
AppCompateDelegate的子类AppCompatDelegateImplV9
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
implements MenuBuilder.Callback, LayoutInflaterFactory {
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
if (!(LayoutInflaterCompat.getFactory(layoutInflater)
instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
}
从这可以看出通过实现LayoutInflaterFactory接口来实现换肤至少可以支持到api 9以上
网上很多换肤框架的实现, 通过LayoutInflater.setFactory的方式, 在回调的onCreateView中解析每一个View的attrs, 判断是否有已标记需要换肤的属性, 比方说background, textColor, 或者说相应资源是否为skin_开头等等.
然后保存到map中, 对每一个View做for循环去遍历所有的attr, 想要对更多的属性进行换肤, 需要Activity实现接口, 将需要换肤的View, 以及相应的属性收集到一起
那么是不是能够寻求一种让使用者更方便的方式来实现, 做一个侵入性尽量小的框架呢?
本着开发者应有的好奇心, 深入的研究了一些v7包的实现
AppCompatDelegateImplV9中, 在LayoutInflaterFactory的接口方法onCreateView 中将View的创建交给了AppCompatViewInflater
@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);
}
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
final boolean isPre21 = Build.VERSION.SDK_INT < 21;
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
// We only want the View to inherit its context if we're running pre-v21
final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
再来看一下AppCompatViewInflater中createView的实现
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
......
View view = null;
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;
......
}
......
return view;
}
再看一下其中一个类AppCompatTextView的实现
public class AppCompatTextView extends TextView implements TintableBackgroundView {
public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper = AppCompatTextHelper.create(this);
mTextHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper.applyCompoundDrawablesTints();
}
@Override
public void setBackgroundResource(@DrawableRes int resId) {
super.setBackgroundResource(resId);
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.onSetBackgroundResource(resId);
}
}
......
}
AppCompatBackgroundHelper.Java
void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
R.styleable.ViewBackgroundHelper, defStyleAttr, 0);
......
if (a.hasValue(R.styleable.ViewBackgroundHelper_android_background)) {
mBackgroundResId = a.getResourceId(
R.styleable.ViewBackgroundHelper_android_background, -1);
ColorStateList tint = mDrawableManager
.getTintList(mView.getContext(), mBackgroundResId);
if (tint != null) {
setInternalBackgroundTint(tint);
}
}
......
}
到这里我仿佛是发现了新大陆一样兴奋, 源码中可以通过拦截View创建过程, 替换一些基础的组件, 然后对一些特殊的属性(eg: background, textColor) 做处理, 那我们为什么不能将这种思想拿到换肤框架中来使用呢?
抱着试一试不会少块肉的心情, 开始了我的换肤框架开发之路
先简单讲一下原理:
1. 参照源码实现在Activity onCreate中为LayoutInflater setFactory, 将View的创建过程交给自定义的SkinCompatViewInflater类来实现
2. 重写系统组件, 实现换肤接口, 表明该控件支持换肤, 并在View创建之后统一收集
3. 在重写的View中解析出需要换肤的属性, 并保存ResId到成员变量
4. 重写类似setBackgroundResource方法, 解析需要换肤的属性, 并保存变量
5. applySkin 在切换皮肤的时候, 从皮肤资源中获取资源
下面说一个简单的例子(SkinCompatTextView):
1. 实现SkinCompatSupportable接口
2. 在构造方法中通过SkinCompatBackgroundHelper和SkinCompatTextHelper分别解析出background, textColor并保存
3. 重写setBackgroundResource和setTextAppearance, 解析出对应的资源Id, 表明该控件支持从代码中设置资源, 且支持该资源换肤
4. 在用户点击切换皮肤时调用applySkin方法设置皮肤
public interface SkinCompatSupportable {
void applySkin();
}
public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {
public SkinCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper = new SkinCompatTextHelper(this);
mTextHelper.loadFromAttributes(attrs, defStyleAttr);
}
@Override
public void setBackgroundResource(@DrawableRes int resId) {
super.setBackgroundResource(resId);
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.onSetBackgroundResource(resId);
}
}
@Override
public void setTextAppearance(Context context, int resId) {
super.setTextAppearance(context, resId);
if (mTextHelper != null) {
mTextHelper.onSetTextAppearance(context, resId);
}
}
@Override
public void applySkin() {
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.applySkin();
}
if (mTextHelper != null) {
mTextHelper.applySkin();
}
}
}
public class SkinCompatTextHelper extends SkinCompatHelper {
private static final String TAG = SkinCompatTextHelper.class.getSimpleName();
private final TextView mView;
private int mTextColorResId = INVALID_ID;
private int mTextColorHintResId = INVALID_ID;
public SkinCompatTextHelper(TextView view) {
mView = view;
}
public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
final Context context = mView.getContext();
// First read the TextAppearance style id
TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
R.styleable.SkinCompatTextHelper, defStyleAttr, 0);
final int ap = a.getResourceId(R.styleable.SkinCompatTextHelper_android_textAppearance, INVALID_ID);
SkinLog.d(TAG, "ap = " + ap);
a.recycle();
if (ap != INVALID_ID) {
a = TintTypedArray.obtainStyledAttributes(context, ap, R.styleable.SkinTextAppearance);
if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {
mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);
}
if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {
mTextColorHintResId = a.getResourceId(
R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);
SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);
}
a.recycle();
}
// Now read the style's values
a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.SkinTextAppearance,
defStyleAttr, 0);
if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {
mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);
}
if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {
mTextColorHintResId = a.getResourceId(
R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);
SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);
}
a.recycle();
applySkin();
}
public void onSetTextAppearance(Context context, int resId) {
final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context,
resId, R.styleable.SkinTextAppearance);
if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {
mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);
}
if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {
mTextColorHintResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);
SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);
}
a.recycle();
applySkin();
}
public void applySkin() {
mTextColorResId = checkResourceId(mTextColorResId);
if (mTextColorResId != INVALID_ID) {
ColorStateList color = SkinCompatResources.getInstance().getColorStateList(mTextColorResId);
mView.setTextColor(color);
}
mTextColorHintResId = checkResourceId(mTextColorHintResId);
if (mTextColorHintResId != INVALID_ID) {
ColorStateList color = SkinCompatResources.getInstance().getColorStateList(mTextColorHintResId);
mView.setHintTextColor(color);
}
}
}
在5.0以上, 使用color为ImageView设置src, 可以通过getColorStateList获取资源, 而在5.0以下, 需要通过ColorDrawable setColor的方式实现
String typeName = mView.getResources().getResourceTypeName(mSrcResId);
if ("color".equals(typeName)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
int color = SkinCompatResources.getInstance().getColor(mSrcResId);
Drawable drawable = mView.getDrawable();
if (drawable instanceof ColorDrawable) {
((ColorDrawable) drawable.mutate()).setColor(color);
} else {
mView.setImageDrawable(new ColorDrawable(color));
}
} else {
ColorStateList colorStateList = SkinCompatResources.getInstance().getColorStateList(mSrcResId);
Drawable drawable = mView.getDrawable();
DrawableCompat.setTintList(drawable, colorStateList);
mView.setImageDrawable(drawable);
}
} else if ("drawable".equals(typeName)) {
Drawable drawable = SkinCompatResources.getInstance().getDrawable(mSrcResId);
mView.setImageDrawable(drawable);
}
还有很多问题, 有兴趣的同学可以来一起交流解决.
这样的做法与网上其他框架相比优势在哪里, 为什么重复造轮子
为什么选择继承自AppCompatActivity, AppCompatTextView…而不是选择直接继承自Activity, TextView…
自定义View能否支持, 第三方控件是否支持换肤
源码地址: https://github.com/ximsfei/Android-skin-support