Android 兩行程式碼實現換膚 從appcompat-v7原理出發
背景
換膚方案原理在網上已經很多了, 這裡不再詳細描述, 強迫症的我總是想讓提供給別人使用的SDK儘量好用, 哪怕是給自己帶來額外的工作量, 經過一段時間的奮鬥, 實現了一個自我感覺良好的換膚框架.
這裡主要來看看Android 原始碼中”com.android.support:appcompat-v7”包的實現, 以及原始碼思想在Android-skin-support中的應用 – 如何打造一款好用的換膚框架.
appcompat-v7包實現
首先來看一下原始碼的實現:
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);
}
....
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
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");
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
從這可以看出通過實現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 */
);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
再來看一下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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
再看一下其中一個類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);
}
}
......
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
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);
}
}
......
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
到這裡我彷彿是發現了新大陸一樣興奮, 原始碼中可以通過攔截View建立過程, 替換一些基礎的元件, 然後對一些特殊的屬性(eg: background, textColor) 做處理, 那我們為什麼不能將這種思想拿到換膚框架中來使用呢?
Android-skin-support換膚框架實現
抱著試一試不會少塊肉的心情, 開始了我的換膚框架開發之路
先簡單講一下原理:
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);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
開發過程中遇到的一些問題
在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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
還有很多問題, 有興趣的同學可以來一起交流解決.
總結
-
這樣的做法與網上其他框架相比優勢在哪裡, 為什麼重複造輪子
- 在增加框架開發成本的基礎上降低了框架使用的成本, 我覺得更有意義, 一次開發, 所有Android 開發者都受用;
- 換膚框架對業務程式碼的侵入性比較小, 業務程式碼只需要繼承自SkinCompatActivity, 不需要實現介面重寫方法, 不需要其他額外的程式碼, 接入方便, 假如將來不想再使用本框架, 只需要把SkinCompatActivity改為原生Activity即可;
- 深入原始碼, 和原始碼實現方式類似, 相容性更好.
-
為什麼選擇繼承自AppCompatActivity, AppCompatTextView…而不是選擇直接繼承自Activity, TextView…
- 本身appcompat-v7包是一個support包, 相容原生控制元件, 同時符合Material design, 我們只需要獲取我們想要換膚的屬性就可以在不破壞support包屬性的前提下進行換膚;
- 參與開發的同學更多的話, 完全可以支援一套繼承自Activity, TextView…的skin support包.
-
自定義View能否支援, 第三方控制元件是否支援換膚
- 答案是肯定的, 完全可以參照SkinCompatTextView的實現, 自己去實現自定義控制元件, 對於使用者來說, 擴充套件性很好.