换肤方案原理在网上已经很多了, 这里不再详细描述, 强迫症的我总是想让提供给别人使用的SDK尽量好用, 哪怕是给自己带来额外的工作量, 经过一段时间的奋斗, 实现了一个自我感觉良好的换肤框架.
这里主要来看看Android 源码中”com.android.support:appcompat-v7”包的实现, 以及源码思想在Android-skin-support中的应用 – 如何打造一款好用的换肤框架.
首先来看一下源码的实现:
AppCompatActivity源码
public class AppCompatActivity extends FragmentActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {final AppCompatDelegate delegate = getDelegate();delegate.installViewFactory();delegate.onCreate(savedInstanceState);...}@Overridepublic MenuInflater getMenuInflater() {return getDelegate().getMenuInflater();}@Overridepublic void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);}@Overridepublic void setContentView(View view) {getDelegate().setContentView(view);}....
}
AppCompatActivity 将大部分生命周期委托给了AppCompatDelegate
再看看相关的类图
AppCompateDelegate的子类AppCompatDelegateImplV9
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBaseimplements MenuBuilder.Callback, LayoutInflaterFactory {@Overridepublic 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 viewfinal View view = callActivityOnCreateView(parent, name, context, attrs);if (view != null) {return view;}// If the Factory didn't handle it, let our createView() method tryreturn createView(parent, name, context, attrs);
}@Override
public View createView(View parent, final String name, &#64;NonNull Context context,&#64;NonNull AttributeSet attrs) {final boolean isPre21 &#61; Build.VERSION.SDK_INT <21;if (mAppCompatViewInflater &#61;&#61; null) {mAppCompatViewInflater &#61; new AppCompatViewInflater();}// We only want the View to inherit its context if we&#39;re running pre-v21final boolean inheritContext &#61; isPre21 && shouldInheritContext((ViewParent) parent);return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,isPre21, /* Only read android:theme pre-L (L&#43; 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, &#64;NonNull Context context,&#64;NonNull AttributeSet attrs, boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {......View view &#61; null;switch (name) {case "TextView":view &#61; new AppCompatTextView(context, attrs);break;case "ImageView":view &#61; new AppCompatImageView(context, attrs);break;case "Button":view &#61; new AppCompatButton(context, attrs);break;case "EditText":view &#61; new AppCompatEditText(context, attrs);break;case "Spinner":view &#61; new AppCompatSpinner(context, attrs);break;case "ImageButton":view &#61; new AppCompatImageButton(context, attrs);break;case "CheckBox":view &#61; 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 &#61; new AppCompatBackgroundHelper(this);mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);mTextHelper &#61; AppCompatTextHelper.create(this);mTextHelper.loadFromAttributes(attrs, defStyleAttr);mTextHelper.applyCompoundDrawablesTints();}&#64;Overridepublic void setBackgroundResource(&#64;DrawableRes int resId) {super.setBackgroundResource(resId);if (mBackgroundTintHelper !&#61; null) {mBackgroundTintHelper.onSetBackgroundResource(resId);}}......
}
AppCompatBackgroundHelper.Java
void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {TintTypedArray a &#61; TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,R.styleable.ViewBackgroundHelper, defStyleAttr, 0);......if (a.hasValue(R.styleable.ViewBackgroundHelper_android_background)) {mBackgroundResId &#61; a.getResourceId(R.styleable.ViewBackgroundHelper_android_background, -1);ColorStateList tint &#61; mDrawableManager.getTintList(mView.getContext(), mBackgroundResId);if (tint !&#61; 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 &#61; new SkinCompatBackgroundHelper(this);mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);mTextHelper &#61; new SkinCompatTextHelper(this);mTextHelper.loadFromAttributes(attrs, defStyleAttr);}&#64;Overridepublic void setBackgroundResource(&#64;DrawableRes int resId) {super.setBackgroundResource(resId);if (mBackgroundTintHelper !&#61; null) {mBackgroundTintHelper.onSetBackgroundResource(resId);}}&#64;Overridepublic void setTextAppearance(Context context, int resId) {super.setTextAppearance(context, resId);if (mTextHelper !&#61; null) {mTextHelper.onSetTextAppearance(context, resId);}}&#64;Overridepublic void applySkin() {if (mBackgroundTintHelper !&#61; null) {mBackgroundTintHelper.applySkin();}if (mTextHelper !&#61; null) {mTextHelper.applySkin();}}
}public class SkinCompatTextHelper extends SkinCompatHelper {private static final String TAG &#61; SkinCompatTextHelper.class.getSimpleName();private final TextView mView;private int mTextColorResId &#61; INVALID_ID;private int mTextColorHintResId &#61; INVALID_ID;public SkinCompatTextHelper(TextView view) {mView &#61; view;}public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {final Context context &#61; mView.getContext();// First read the TextAppearance style idTintTypedArray a &#61; TintTypedArray.obtainStyledAttributes(context, attrs,R.styleable.SkinCompatTextHelper, defStyleAttr, 0);final int ap &#61; a.getResourceId(R.styleable.SkinCompatTextHelper_android_textAppearance, INVALID_ID);SkinLog.d(TAG, "ap &#61; " &#43; ap);a.recycle();if (ap !&#61; INVALID_ID) {a &#61; TintTypedArray.obtainStyledAttributes(context, ap, R.styleable.SkinTextAppearance);if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {mTextColorResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);SkinLog.d(TAG, "mTextColorResId &#61; " &#43; mTextColorResId);}if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {mTextColorHintResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);SkinLog.d(TAG, "mTextColorHintResId &#61; " &#43; mTextColorHintResId);}a.recycle();}// Now read the style&#39;s valuesa &#61; TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.SkinTextAppearance,defStyleAttr, 0);if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {mTextColorResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);SkinLog.d(TAG, "mTextColorResId &#61; " &#43; mTextColorResId);}if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {mTextColorHintResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);SkinLog.d(TAG, "mTextColorHintResId &#61; " &#43; mTextColorHintResId);}a.recycle();applySkin();}public void onSetTextAppearance(Context context, int resId) {final TintTypedArray a &#61; TintTypedArray.obtainStyledAttributes(context,resId, R.styleable.SkinTextAppearance);if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {mTextColorResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);SkinLog.d(TAG, "mTextColorResId &#61; " &#43; mTextColorResId);}if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {mTextColorHintResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);SkinLog.d(TAG, "mTextColorHintResId &#61; " &#43; mTextColorHintResId);}a.recycle();applySkin();}public void applySkin() {mTextColorResId &#61; checkResourceId(mTextColorResId);if (mTextColorResId !&#61; INVALID_ID) {ColorStateList color &#61; SkinCompatResources.getInstance().getColorStateList(mTextColorResId);mView.setTextColor(color);}mTextColorHintResId &#61; checkResourceId(mTextColorHintResId);if (mTextColorHintResId !&#61; INVALID_ID) {ColorStateList color &#61; SkinCompatResources.getInstance().getColorStateList(mTextColorHintResId);mView.setHintTextColor(color);}}
}
在5.0以上, 使用color为ImageView设置src, 可以通过getColorStateList获取资源, 而在5.0以下, 需要通过ColorDrawable setColor的方式实现
String typeName &#61; mView.getResources().getResourceTypeName(mSrcResId);
if ("color".equals(typeName)) {if (Build.VERSION.SDK_INT
} else if ("drawable".equals(typeName)) {Drawable drawable &#61; SkinCompatResources.getInstance().getDrawable(mSrcResId);mView.setImageDrawable(drawable);
}
还有很多问题, 有兴趣的同学可以来一起交流解决.
这样的做法与网上其他框架相比优势在哪里, 为什么重复造轮子
为什么选择继承自AppCompatActivity, AppCompatTextView…而不是选择直接继承自Activity, TextView…
自定义View能否支持, 第三方控件是否支持换肤
源码地址: https://github.com/ximsfei/Android-skin-support