1. 背景
会社のビジネスではスキニングを使用する必要があります。二輪車を繰り返さないように、需要を迅速に実現し、安定性を求めるために、Github に行き、星の数が多いスキニング フレームワークを見つけました。
Android-skin-support
ハート スキニング フレームワーク、非常に低い学習コスト、優れたユーザー エクスペリエンス。1 行のコードでスキニングを実現できます。それだけの価値があります!!!) 簡単に理解した後は、すぐに開始でき、侵入は非常に低くなります。もちろん、真に柔軟性を持たせるためには、その背後にある原則を理解する必要があります。また、バグがある場合は、すぐに問題を特定できるため、その後の会社のプロジェクトのメンテナンスに非常に役立ちます。ここでは原則についてのみ説明します。具体的な使用方法は、公式ドキュメントのソースコードのアドレス: https://github.com/ximsfei/Android-skin-support にアクセスしてください。
2. AppCompatActivity の実装
始める前に、いくつかの予備知識を取得し、AppCompatActivity の実装を見てみましょう。これは、後でフレームワークの原則を理解するのに非常に役立ちます。
public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
if (delegate.applyDayNight() && mThemeId != 0) {
// If DayNight has been applied, we need to re-apply the theme for
// the changes to take effect. On API 23+, we should bypass
// setTheme(), which will no-op if the theme ID is identical to the
// current theme ID.
if (Build.VERSION.SDK_INT >= 23) {
onApplyThemeResource(getTheme(), mThemeId, false);
} else {
setTheme(mThemeId);
}
}
super.onCreate(savedInstanceState);
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
protected void onStart() {
super.onStart();
getDelegate().onStart();
}
@Override
protected void onStop() {
super.onStop();
getDelegate().onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
getDelegate().onDestroy();
}
@Override
protected void onTitleChanged(CharSequence title, int color) {
super.onTitleChanged(title, color);
getDelegate().setTitle(title);
}
......
}
AppCompatDelegate があることがわかりますが、これは何に使われるのでしょうか? 情報によると、これは Activity のデリゲートであり、AppCompatActivity はそのライフ サイクルのほとんどを AppCompatDelegate に委任していることがわかります (上記のソース コードからわかります)。 AppCompatDelegate のソース コードを調べたところ、そのクラス アノテーションもこの方法で記述されていることがわかりました。
次に、AppCompatDelegate の作成を見てみましょう。
AppCompatActivity.java
/**
* @return The {@link AppCompatDelegate} being used by this Activity.
*/
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
AppCompatDelegate.java
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return create(activity, activity.getWindow(), callback);
}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
if (Build.VERSION.SDK_INT >= 24) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
APIバージョン番号ごとに使用するAppCompatDelegateが異なりますクラスの継承関係図は以下となります
上記はdelegate.installViewFactory();
実際にAppCompatDelegateImplV9で実装されているので、ソースコードを見てください。
AppCompatDelegateImplV9.java
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
LayoutInflaterCompat.setFactory2(layoutInflater, this);
最後に、 LayoutInflater を呼び出すメソッドのsetFactory2()
実装を見てみましょう。
/**
* Like {@link #setFactory}, but allows you to set a {@link Factory2}
* interface.
*/
public void setFactory2(Factory2 factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
ここで少し詳しく説明します。Factory2 は 1 回しか設定できません。設定が完了すると、mFactorySet プロパティは true になり、次回設定するときに直接例外がスローされます。では、Factory2 はどのような用途に使用されるのでしょうか
?その実装
public interface Factory2 extends Factory {
/**
* Version of {@link #onCreateView(String, Context, AttributeSet)}
* that also supplies the parent that the view created view will be
* placed in.
*
* @param parent The parent that the created view will be placed
* in; <em>note that this may be null</em>.
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
View を作成するためのメソッドが 1 つだけあるインターフェイスですが、そうですか? 答えは後ほど判明します。
AppCompatActivity はデリゲートを設定し、mFactory2 を LayoutInflater に設定します。今のところはこれだけで十分です。
3. Android で View を作成するプロセス全体の分析
Android が XML レイアウトに基づいてビューを作成する方法を見てみましょう
通常、最もよく使用されるアクティビティで setContentView() を使用してレイアウト ID を設定します。アクティビティの実装を参照してください。
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
呼び出されるのは Window の setContentView() ですが、Window には PhoneWindow という 1 つの実装クラスしかありません。
@Override
public void setContentView(int layoutResID) {
...
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
...
}
今日の主役 mLayoutInflater を見ましたが、mLayoutInflater は PhoneWindow の構築メソッドで初期化されています。このレイアウト (layoutResID) を読み込むには mLayoutInflater を使用します。クリックすると実装が表示されます
レイアウトインフレータ.java
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
XML パーサーがレイアウトを使用して作成され、解析されることがわかります。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
...
}
実際、キーとなるコードは 2 つだけだと思います。xml に書かれた内容に従って View を構築することです。rInflateChildren()
最終的に メソッドが呼び出されますcreateViewFromTag()
。ここでは、最初に rootView を作成し、次に、子ビューを rootView に渡します。
createViewFromTag() の実装を見てみましょう。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
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);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
...
return view;
}
mFactory2 が空でない場合、mFactory2 が呼び出されてビュー (mFactory2.onCreateView(parent, name, context, attrs)) が作成されることがわかります。この結論は非常に重要です。前の答えは明らかにされています。 set ビューの作成には mFactory2 を使用します。上記で mFactory2 が設定されていますが、実際には mFactory2 は AppCompatDelegateImplV9 ですAppCompatDelegateImplV9
。installViewFactory()
createView() の具体的な実装を見てみましょう。
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
inheritContext = (attrs instanceof XmlPullParser)
// If we have a XmlPullParser, we can detect where we are in the layout
? ((XmlPullParser) attrs).getDepth() > 1
// Otherwise we have to use the old heuristic
: shouldInheritContext((ViewParent) parent);
}
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* 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()
Viewを作成していることが分かりますが、オブジェクト指向の5原則の一つである単一責任原則の一つであるViewの作成にAppCompatViewInflaterを特別に使用しているような気がします。
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;
// We need to 'inject' our tint aware Views in place of the standard framework versions
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;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
TextView
コントロールが xml で書かれている場合、それは実際には作成したコントロールの名前によって決定され、新しい方法で作成され、新しいものは TextView ではないことがわかります。その他のシステムAppCompatTextView
コントロールも新しくなりました。
ただし、問題があります。これらのコントロール (RecyclerView、カスタム コントロールなど) を XML レイアウトに記述しない場合、ビューはどのように作成すればよいですか? switch ブロックの実行後にビューが空である場合に注意してください。コード (列内の上記のコントロールではありません) では、メソッドが呼び出されますcreateViewFromTag()
。実装を見てみましょう。
private static final String[] sClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;
//这里判断一下name(即在xml中写的控件名称)中是否含有'.'
//如果没有那么肯定就是系统控件(比如ProgressBar,在布局中是不需要加ProgressBar的具体包名的)
//如果有那么就是自定义控件,或者是系统的控件(比如android.support.v7.widget.SwitchCompat)
if (-1 == name.indexOf('.')) {
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createView(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {
return createView(context, name, null);
}
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
} finally {
// Don't retain references on context.
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
ここでさらに興味深いのは、まずシステム コントロールかどうかを判断することですが、どのように判断するかというと、コントロールの名前に '.' が含まれているかどうかで判断します。システム コントロールが XML レイアウトで宣言されている場合、それは次のようになります。 ProgressBar などの特定のパッケージ名を追加する必要はありません。したがって、「.」のないものはシステム コントロールである必要があります。その後、「.」の付いているものはカスタム コントロールまたは特別なシステム コントロール (android.support.v7.widget.SwitchCompat など) です。 )。
ちょっとした質問があります?なぜシステム コントロールはレイアウトで宣言するときにパッケージ名を追加できないのに、カスタム コントロールはパッケージ名を追加する必要があるのですか?
実際、ほとんどのシステム コントロールは sClassPrefixList で定義されたパッケージ名の下に配置されるため、後で結合することでコントロールの位置を見つけることができます。ランダムな例を使用して、どのシステム コントロールがパッケージの下にあるかを見てみましょandroid.widget.
う
ソース コード内でシステム コントロールと非システム コントロールを別々に作成します。実際、メソッドは同じですが、プレフィックスが付くものとプレフィックスがないものがあります。作成メソッドの実装を見てみましょう
private static final Class<?>[] sConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
private static final Map<String, Constructor<? extends View>> sConstructorMap
= new ArrayMap<>();
private View createView(Context context, String name, String prefix)
throws ClassNotFoundException, InflateException {
//这里的sConstructorMap是用来做缓存的,如果之前已经创建,则会将构造方法缓存起来,下次直接用
Constructor<? extends View> constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
//通过classLoader去寻找该class,这里的classLoader其实是PathClassLoader
//看到没? (prefix + name)这种直接将前缀与名称拼接的方式就可以将View的位置拼接出来
//然后其他的全类名的View就不需要拼接前缀
Class<? extends View> clazz = context.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
//获取构造方法
constructor = clazz.getConstructor(sConstructorSignature);
//缓存构造方法
sConstructorMap.put(name, constructor);
}
//设置构造方法可访问
constructor.setAccessible(true);
//通过构造方法new一个View对象出来
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
}
}
実際、View の作成は、ClassLoader を使用してこのクラスのクラスを検索し、その{ Context.class, AttributeSet.class}
構築メソッドを取得し、リフレクションを通じて View を作成することであり、その具体的なロジックはコード内にマークされています。
これまでのところ、Android コントロールの読み込み方法は完全に分析されています。
その中には、スムーズなフローのために上記では触れなかった細かい点があり、次のような View を構築する (コントロール名に従って AppCompatXX コントロールを作成する) ためのコードがあります。
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;
}
コントロールのソース コードを見てみましょう。たとえばAppCompatTextView
、他の AppCompatXX コントロールの実装も同様です。
public class AppCompatTextView extends TextView implements TintableBackgroundView,
AutoSizeableTextView {
//这2个是关键类
private final AppCompatBackgroundHelper mBackgroundTintHelper;
private final AppCompatTextHelper mTextHelper;
public AppCompatTextView(Context context) {
this(context, null);
}
public AppCompatTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
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();
}
......
class AppCompatBackgroundHelper {
......
void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
R.styleable.ViewBackgroundHelper, defStyleAttr, 0);
try {
if (a.hasValue(R.styleable.ViewBackgroundHelper_android_background)) {
//获取android:background 背景的资源id
mBackgroundResId = a.getResourceId(
R.styleable.ViewBackgroundHelper_android_background, -1);
ColorStateList tint = mDrawableManager
.getTintList(mView.getContext(), mBackgroundResId);
if (tint != null) {
setInternalBackgroundTint(tint);
}
}
if (a.hasValue(R.styleable.ViewBackgroundHelper_backgroundTint)) {
//获取android:backgroundTint
ViewCompat.setBackgroundTintList(mView,
a.getColorStateList(R.styleable.ViewBackgroundHelper_backgroundTint));
}
if (a.hasValue(R.styleable.ViewBackgroundHelper_backgroundTintMode)) {
//获取android:backgroundTintMode
ViewCompat.setBackgroundTintMode(mView,
DrawableUtils.parseTintMode(
a.getInt(R.styleable.ViewBackgroundHelper_backgroundTintMode, -1),
null));
}
} finally {
a.recycle();
}
}
}
class AppCompatTextHelper {
......
@SuppressLint("NewApi")
void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
final Context context = mView.getContext();
final AppCompatDrawableManager drawableManager = AppCompatDrawableManager.get();
// First read the TextAppearance style id
TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
R.styleable.AppCompatTextHelper, defStyleAttr, 0);
final int ap = a.getResourceId(R.styleable.AppCompatTextHelper_android_textAppearance, -1);
// Now read the compound drawable and grab any tints
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableLeft)) {
mDrawableLeftTint = createTintInfo(context, drawableManager,
a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableLeft, 0));
}
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableTop)) {
mDrawableTopTint = createTintInfo(context, drawableManager,
a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableTop, 0));
}
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableRight)) {
mDrawableRightTint = createTintInfo(context, drawableManager,
a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableRight, 0));
}
if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableBottom)) {
mDrawableBottomTint = createTintInfo(context, drawableManager,
a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableBottom, 0));
}
a.recycle();
// PasswordTransformationMethod wipes out all other TransformationMethod instances
// in TextView's constructor, so we should only set a new transformation method
// if we don't have a PasswordTransformationMethod currently...
final boolean hasPwdTm =
mView.getTransformationMethod() instanceof PasswordTransformationMethod;
boolean allCaps = false;
boolean allCapsSet = false;
ColorStateList textColor = null;
ColorStateList textColorHint = null;
ColorStateList textColorLink = null;
// First check TextAppearance's textAllCaps value
if (ap != -1) {
a = TintTypedArray.obtainStyledAttributes(context, ap, R.styleable.TextAppearance);
if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) {
allCapsSet = true;
allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false);
}
updateTypefaceAndStyle(context, a);
if (Build.VERSION.SDK_INT < 23) {
// If we're running on < API 23, the text color may contain theme references
// so let's re-set using our own inflater
if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor);
}
if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) {
textColorHint = a.getColorStateList(
R.styleable.TextAppearance_android_textColorHint);
}
if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) {
textColorLink = a.getColorStateList(
R.styleable.TextAppearance_android_textColorLink);
}
}
a.recycle();
}
// Now read the style's values
a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.TextAppearance,
defStyleAttr, 0);
if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) {
allCapsSet = true;
allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false);
}
if (Build.VERSION.SDK_INT < 23) {
// If we're running on < API 23, the text color may contain theme references
// so let's re-set using our own inflater
if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor);
}
if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) {
textColorHint = a.getColorStateList(
R.styleable.TextAppearance_android_textColorHint);
}
if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) {
textColorLink = a.getColorStateList(
R.styleable.TextAppearance_android_textColorLink);
}
}
updateTypefaceAndStyle(context, a);
a.recycle();
if (textColor != null) {
mView.setTextColor(textColor);
}
if (textColorHint != null) {
mView.setHintTextColor(textColorHint);
}
if (textColorLink != null) {
mView.setLinkTextColor(textColorLink);
}
if (!hasPwdTm && allCapsSet) {
setAllCaps(allCaps);
}
if (mFontTypeface != null) {
mView.setTypeface(mFontTypeface, mStyle);
}
mAutoSizeTextHelper.loadFromAttributes(attrs, defStyleAttr);
if (PLATFORM_SUPPORTS_AUTOSIZE) {
// Delegate auto-size functionality to the framework implementation.
if (mAutoSizeTextHelper.getAutoSizeTextType()
!= TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE) {
final int[] autoSizeTextSizesInPx =
mAutoSizeTextHelper.getAutoSizeTextAvailableSizes();
if (autoSizeTextSizesInPx.length > 0) {
if (mView.getAutoSizeStepGranularity() != AppCompatTextViewAutoSizeHelper
.UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
// Configured with granularity, preserve details.
mView.setAutoSizeTextTypeUniformWithConfiguration(
mAutoSizeTextHelper.getAutoSizeMinTextSize(),
mAutoSizeTextHelper.getAutoSizeMaxTextSize(),
mAutoSizeTextHelper.getAutoSizeStepGranularity(),
TypedValue.COMPLEX_UNIT_PX);
} else {
mView.setAutoSizeTextTypeUniformWithPresetSizes(
autoSizeTextSizesInPx, TypedValue.COMPLEX_UNIT_PX);
}
}
}
}
}
こことここで、Android のソース コードは本当に素晴らしいと言わざるを得ません。また、単一責任の原則を体現しています。どこに尋ねますか? システムはバックグラウンド関連のものを AppCompatBackgroundHelper に処理し、テキストはAppCompatTextHelper に関連するもの。
AppCompatBackgroundHelper と AppCompatTextHelper は、xml で定義された属性の値を取得した後、その値をコントロールに割り当てます。
これを見て、ほぼ予備知識が紹介されているのですが、長く読んでいると、あなたが言ったこれらの厄介なことは、私の肌の変化に関係があるのですか?
あなたの手にあるレンガを置いて、哀れな道士の話を聞いてください。
4. 皮膚変化原理の詳細解析
1. 上記予備知識と肌変化の関係
ソース コードでは、ビューの作成プロセスをインターセプトし、いくつかの基本コンポーネント (例: TextView -> AppCompatTextView
) を置き換えてから、いくつかの特別な属性 (例: 背景、テキストカラー) を処理できます。なぜこのアイデアをスキニング フレームワークで使用できないのでしょうか。 ? どうですか? 一言で夢想家を起こしてあげるよ、兄弟。コミッションを作ることもできますし、AppCompatViewInflater に似たコントロール ローダーを作ることもできますし、ビューの作成と同等の mFactory2 を設定することもできます。プロセスを引き継ぎます。引き継いだので、すべてのコントロールでやりたいことができるようになりますか???? もちろんです。やりたいことができるので、スキンを変更するのは非常に簡単です。
2. ソースコード 1、コントロール作成の全プロセス
SkinCompatManager.withoutActivity(application)
.addInflater(new SkinAppCompatViewInflater());
まず、ライブラリの初期化から始めます。ここでは、Application が渡され、SkinAppCompatViewInflater が追加されます。SkinAppCompatViewInflater は、実際に View を作成するために使用されます。これは、システムの AppCompatViewInflater に似ています。それが何をwithoutActivity(application)
する
//SkinCompatManager.java
public static SkinCompatManager withoutActivity(Application application) {
init(application);
SkinActivityLifecycle.init(application);
return sInstance;
}
//SkinActivityLifecycle.java
public static SkinActivityLifecycle init(Application application) {
if (sInstance == null) {
synchronized (SkinActivityLifecycle.class) {
if (sInstance == null) {
sInstance = new SkinActivityLifecycle(application);
}
}
}
return sInstance;
}
private SkinActivityLifecycle(Application application) {
//就是这里,注册了ActivityLifecycleCallbacks,可以监听所有Activity的生命周期
application.registerActivityLifecycleCallbacks(this);
//这个方法稍后看
installLayoutFactory(application);
SkinCompatManager.getInstance().addObserver(getObserver(application));
}
ActivityLifecycleCallback が初期化中に実際に SkinActivityLifecycle に登録されていることがわかり、アプリ内のすべてのアクティビティのライフサイクルを監視できるようになりました。
SkinActivityLifecycle で Activity の onCreate() メソッドが監視されるときに何が行われるかを見てみましょう。
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
//判断是否需要换肤 这个可以外部初始化时控制
if (isContextSkinEnable(activity)) {
//在Activity创建的时候,直接将Factory设置成三方库里面的
installLayoutFactory(activity);
//更新状态栏颜色
updateStatusBarColor(activity);
//更新window背景颜色
updateWindowBackground(activity);
if (activity instanceof SkinCompatSupportable) {
((SkinCompatSupportable) activity).applySkin();
}
}
}
/**
* 设置Factory(创建View的工厂)
*/
private void installLayoutFactory(Context context) {
LayoutInflater layoutInflater = LayoutInflater.from(context);
try {
//setFactory只能调用一次,用于设置Factory(创建View), 设置了Factory了mFactorySet就会是true
//如果需要重新设置Factory,则需要先将mFactorySet设置为false,不然系统判断到mFactorySet是true则会抛异常.
//这里使用自己构建的Factory去创建View,在创建View时当然也就可以控制它的背景或者文字颜色.
//(在这里之前需要知道哪些控件需要换肤,其中一部分是继承自三方库的控件,这些控件是实现了SkinCompatSupportable接口的,可以很方便的控制.
// 还有一部分是系统的控件,在创建时直接创建三方库中的控件(比如View就创建SkinCompatView).
// 在设置系统控件的背景颜色和文字颜色时,直接从三方库缓存颜色中取值,然后进行设置.)
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater, false);
LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));
} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
アクティビティを作成したら、まずスキンを変更する必要があるかどうかを判断し、変更する必要がある場合は変更します。
installLayoutFactory()
メソッドに焦点を当てます。上記の予備知識では、mFactory
一度しか設定できないと例外がスローされるため、例外が発生しmFactorySet
ない前にエミッションを使用して値を false に設定する必要があります。そうすれば実行できますsetFactory()
。
2 番目のパラメーターの作成プロセスを見てみましょうsetFactory()
2 番目のパラメーターは、実際には View を作成するためのファクトリーです。
//SkinActivityLifecycle.java
private SkinCompatDelegate getSkinDelegate(Context context) {
if (mSkinDelegateMap == null) {
mSkinDelegateMap = new WeakHashMap<>();
}
SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);
if (mSkinDelegate == null) {
mSkinDelegate = SkinCompatDelegate.create(context);
mSkinDelegateMap.put(context, mSkinDelegate);
}
return mSkinDelegate;
}
//SkinCompatDelegate.java
public class SkinCompatDelegate implements LayoutInflaterFactory {
private final Context mContext;
//主角 在这里 在这里!!!
private SkinCompatViewInflater mSkinCompatViewInflater;
private List<WeakReference<SkinCompatSupportable>> mSkinHelpers = new ArrayList<>();
private SkinCompatDelegate(Context context) {
mContext = context;
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = createView(parent, name, context, attrs);
if (view == null) {
return null;
}
if (view instanceof SkinCompatSupportable) {
mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
}
return view;
}
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mSkinCompatViewInflater == null) {
mSkinCompatViewInflater = new SkinCompatViewInflater();
}
List<SkinWrapper> wrapperList = SkinCompatManager.getInstance().getWrappers();
for (SkinWrapper wrapper : wrapperList) {
Context wrappedContext = wrapper.wrapContext(mContext, parent, attrs);
if (wrappedContext != null) {
context = wrappedContext;
}
}
return mSkinCompatViewInflater.createView(parent, name, context, attrs);
}
public static SkinCompatDelegate create(Context context) {
return new SkinCompatDelegate(context);
}
public void applySkin() {
if (mSkinHelpers != null && !mSkinHelpers.isEmpty()) {
for (WeakReference ref : mSkinHelpers) {
if (ref != null && ref.get() != null) {
((SkinCompatSupportable) ref.get()).applySkin();
}
}
}
}
}
SkinCompatDelegate は SkinCompatViewInflater のデリゲートであることがわかりますが、これは実際にはシステムの AppCompatDelegateImplV9 に非常によく似ています。
システムが View を作成する必要がある場合、LayoutInflater の Factory が SkinCompatDelegate に設定されているため、SkinCompatDelegate のメソッドがコールバックされ、@Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs)
SkinCompatDelegate は View の作成作業を SkinCompatViewInflater に引き渡します (システムとまったく同じです)。 。
SkinCompatViewInflater がどのようにビューを作成するかを見てみましょう
public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
View view = createViewFromHackInflater(context, name, attrs);
if (view == null) {
view = createViewFromInflater(context, name, attrs);
}
if (view == null) {
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check it's android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
private View createViewFromInflater(Context context, String name, AttributeSet attrs) {
View view = null;
//这里的SkinLayoutInflater(我理解为控件创建器)就是我们之前在初始化时设置的SkinAppCompatViewInflater
//当然,SkinLayoutInflater可以有多个
for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getInflaters()) {
view = inflater.createView(context, name, attrs);
if (view == null) {
continue;
} else {
break;
}
}
return view;
}
//这个方法和系统的完全一模一样嘛,so easy
public View createViewFromTag(Context context, String name, AttributeSet attrs) {
if ("view".equals(name)) {
name = attrs.getAttributeValue(null, "class");
}
try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;
//自定义控件
if (-1 == name.indexOf('.')) {
for (int i = 0; i < sClassPrefixList.length; i++) {
final View view = createView(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {
return createView(context, name, null);
}
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
} finally {
// Don't retain references on context.
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
これらの実装は、実際にはシステムの実装と同様であることがわかりますが、その原理は上記の予備知識で示されています。
ここにも違いがあり、createViewFromInflater()
このメソッドでは、ライブラリを初期化してビューを作成するときに設定した SkinLayoutInflater (コントロール作成者だと思います) を使用します。
なぜ SkinCompatViewInflater を改良する必要があり、さらに詳細な SkinLayoutInflater で処理する必要があるのでしょうか? 拡張の都合によるものだと思います。ライブラリには、SkinAppCompatViewInflater (基本コントロール ビルダー)、SkinmaterialViewInflater (マテリアル デザイン コントロール コンストラクター)、SkinConstraintViewInflater (ConstraintLayout ビルダー)、SkinCardViewInflater (CardView v7 ビルダー)。
初期化時に SkinAppCompatViewInflater を設定しているため、他のビルダーも原理的には同様です。
//SkinAppCompatViewInflater.java
@Override
public View createView(Context context, String name, AttributeSet attrs) {
View view = createViewFromFV(context, name, attrs);
if (view == null) {
view = createViewFromV7(context, name, attrs);
}
return view;
}
private View createViewFromFV(Context context, String name, AttributeSet attrs) {
View view = null;
if (name.contains(".")) {
return null;
}
switch (name) {
case "View":
view = new SkinCompatView(context, attrs);
break;
case "LinearLayout":
view = new SkinCompatLinearLayout(context, attrs);
break;
case "RelativeLayout":
view = new SkinCompatRelativeLayout(context, attrs);
break;
case "FrameLayout":
view = new SkinCompatFrameLayout(context, attrs);
break;
case "TextView":
view = new SkinCompatTextView(context, attrs);
break;
case "ImageView":
view = new SkinCompatImageView(context, attrs);
break;
case "Button":
view = new SkinCompatButton(context, attrs);
break;
case "EditText":
view = new SkinCompatEditText(context, attrs);
break;
......
default:
break;
}
return view;
}
private View createViewFromV7(Context context, String name, AttributeSet attrs) {
View view = null;
switch (name) {
case "android.support.v7.widget.Toolbar":
view = new SkinCompatToolbar(context, attrs);
break;
default:
break;
}
return view;
}
柳と花が別の村を明るくします? これは、Android のソース コードで以前に見たコードではないでしょうか? ほぼ同じです。ここでビューの作成をインターセプトし、独自のコントロールを作成します。自分たちで作ったコントロールなので、やりたいことが簡単にできるのではないでしょうか?
SkinCompatTextView
ソースコードを見てみましょう
//SkinCompatTextView.java
public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {
private SkinCompatTextHelper mTextHelper;
private SkinCompatBackgroundHelper mBackgroundTintHelper;
public SkinCompatTextView(Context context) {
this(context, null);
}
public SkinCompatTextView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public SkinCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
mTextHelper = SkinCompatTextHelper.create(this);
mTextHelper.loadFromAttributes(attrs, defStyleAttr);
}
......
@Override
public void applySkin() {
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.applySkin();
}
if (mTextHelper != null) {
mTextHelper.applySkin();
}
}
}
これは依然として古典的な操作であり、背景関連のプロパティを処理のために SkinCompatBackgroundHelper に渡し、textColor 関連の操作を処理のために SkinCompatTextHelper に渡します。ソースコードとまったく同じです。
3. ソースコード 2、スキンパッケージからスキンをロードします
実際、スキン パッケージは APK ですが、その中にはコードはなく、変更する必要があるのは一部のリソースまたは色だけです。また、これらのリソースの名前は、変更する前に現在のアプリのリソース名と一致している必要があります。必要なスキン リソースは、スキン バッグに直接移動して取得します。
使い方
SkinCompatManager.getInstance().loadSkin("night.skin", null, CustomSDCardLoader.SKIN_LOADER_STRATEGY_SDCARD);
さあ、loadSkin() メソッドを見てみましょう。
/**
* 加载皮肤包.
* @param skinName 皮肤包名称.
* @param listener 皮肤包加载监听.
* @param strategy 皮肤包加载策略.
*/
public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) {
//加载策略 分为好几种:从SD卡中加载皮肤,从assets文件中加载皮肤等等
SkinLoaderStrategy loaderStrategy = mStrategyMap.get(strategy);
if (loaderStrategy == null) {
return null;
}
return new SkinLoadTask(listener, loaderStrategy).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, skinName);
}
SkinLoadTask は AsyncTask である必要があることがわかり、バックグラウンドでスキン パッケージを解析します。これは AsyncTask であるため、メソッドを確認する必要がありますdoInBackground()
。
SkinLoadTask を見てみましょうdoInBackground()
//SkinLoadTask.java
@Override
protected String doInBackground(String... params) {
......
try {
if (params.length == 1) {
//根据加载策略去后台加载皮肤
String skinName = mStrategy.loadSkinInBackground(mAppContext, params[0]);
if (TextUtils.isEmpty(skinName)) {
SkinCompatResources.getInstance().reset(mStrategy);
}
return params[0];
}
} catch (Exception e) {
e.printStackTrace();
}
SkinCompatResources.getInstance().reset();
return null;
}
//加载策略 随便挑一个吧 SkinSDCardLoader.java 从SD卡加载皮肤
@Override
public String loadSkinInBackground(Context context, String skinName) {
if (TextUtils.isEmpty(skinName)) {
return skinName;
}
//获取皮肤路径
String skinPkgPath = getSkinPath(context, skinName);
if (SkinFileUtils.isFileExists(skinPkgPath)) {
//获取皮肤包包名.
String pkgName = SkinCompatManager.getInstance().getSkinPackageName(skinPkgPath);
//获取皮肤包的Resources
Resources resources = SkinCompatManager.getInstance().getSkinResources(skinPkgPath);
if (resources != null && !TextUtils.isEmpty(pkgName)) {
SkinCompatResources.getInstance().setupSkin(
resources,
pkgName,
skinName,
this);
return skinName;
}
}
return null;
}
//SkinCompatManager.java
//获取皮肤包包名.
public String getSkinPackageName(String skinPkgPath) {
PackageManager mPm = mAppContext.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
return info.packageName;
}
//获取皮肤包资源{@link Resources}.
@Nullable
public Resources getSkinResources(String skinPkgPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
Resources superRes = mAppContext.getResources();
return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
おそらく、スキン パッケージのパッケージ名とリソースを取得するためにサブスレッドに移動します (これは何のために必要ですか? 後でこれを通じてスキン パッケージ内の色またはリソースを取得する必要があります)。
SkinCompatResources.getInstance().setupSkin()
方法としては、スキンパッケージからロードしたリソース、パッケージ名、スキン名、ロードストラテジをすべて保存し、後でスキンパッケージ内のリソースを取得できるようにします。
ライブラリで定義されているコントロールはすべて、スキニングの制御に便利な SkinCompatSupportable インターフェイスを実装しています。たとえば、SkinCompatTextView の applySkin() メソッドは、BackgroundTintHelper メソッドと TextHelperapplySkin()
メソッドを呼び出します。これは、スキンの変更時に背景またはテキストの色が動的に変更されることを意味します。mBackgroundTintHelper.applySkin()
実装を見てみましょう
//SkinCompatBackgroundHelper.java
@Override
public void applySkin() {
//该控件是否有背景 检测
mBackgroundResId = checkResourceId(mBackgroundResId);
if (mBackgroundResId == INVALID_ID) {
return;
}
Drawable drawable = SkinCompatVectorResources.getDrawableCompat(mView.getContext(), mBackgroundResId);
if (drawable != null) {
int paddingLeft = mView.getPaddingLeft();
int paddingTop = mView.getPaddingTop();
int paddingRight = mView.getPaddingRight();
int paddingBottom = mView.getPaddingBottom();
ViewCompat.setBackground(mView, drawable);
mView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
}
}
ドローアブルを取得して、ビューの背景を設定します。ここで重要なのは、ドローアブルをどのように取得するかです。具体的な実装を見てみましょう。
//SkinCompatVectorResources.java
private Drawable getSkinDrawableCompat(Context context, int resId) {
//当前是非默认皮肤
if (!SkinCompatResources.getInstance().isDefaultSkin()) {
try {
return SkinCompatDrawableManager.get().getDrawable(context, resId);
} catch (Exception e) {
e.printStackTrace();
}
}
......
return AppCompatResources.getDrawable(context, resId);
}
//SkinCompatDrawableManager.java
public Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) {
return getDrawable(context, resId, false);
}
Drawable getDrawable(@NonNull Context context, @DrawableRes int resId,
boolean failIfNotKnown) {
//检查Drawable是否能被正确解码
checkVectorDrawableSetup(context);
Drawable drawable = loadDrawableFromDelegates(context, resId);
if (drawable == null) {
drawable = createDrawableIfNeeded(context, resId);
}
if (drawable == null) {
//这里是关键
drawable = SkinCompatResources.getDrawable(context, resId);
}
if (drawable != null) {
// Tint it if needed
drawable = tintDrawable(context, resId, failIfNotKnown, drawable);
}
if (drawable != null) {
// See if we need to 'fix' the drawable
SkinCompatDrawableUtils.fixDrawable(drawable);
}
return drawable;
}
最後に、SkinCompatDrawableManager が呼び出されてドローアブルを取得します。この SkinCompatDrawableManager はシステムの AppCompatDrawableManager とまったく同じであることがわかりました。唯一の違いは、上記の 31 行目で、
ドローアブルdrawable = SkinCompatResources.getDrawable(context, resId);
を作成するときに SkinCompatResources を使用してそれを取得していることです。
SkinCompatResources を覚えていますか? つまり、上記のスキン パッケージの情報を取得した後、すべての情報をこのクラスに保存しました。
//SkinCompatResources.java
//皮肤的Resources可以通过它来获取皮肤里面的资源
private Resources mResources;
//皮肤包名
private String mSkinPkgName = "";
//皮肤名
private String mSkinName = "";
//加载策略
private SkinCompatManager.SkinLoaderStrategy mStrategy;
//是默认皮肤?
private boolean isDefaultSkin = true;
public static Drawable getDrawable(Context context, int resId) {
return getInstance().getSkinDrawable(context, resId);
}
/**
* 通过id获取皮肤中的drawable资源
* @param context Context
* @param resId 资源id
*/
private Drawable getSkinDrawable(Context context, int resId) {
//是否有皮肤颜色缓存
if (!SkinCompatUserThemeManager.get().isColorEmpty()) {
ColorStateList colorStateList = SkinCompatUserThemeManager.get().getColorStateList(resId);
if (colorStateList != null) {
return new ColorDrawable(colorStateList.getDefaultColor());
}
}
//是否有皮肤drawable缓存
if (!SkinCompatUserThemeManager.get().isDrawableEmpty()) {
Drawable drawable = SkinCompatUserThemeManager.get().getDrawable(resId);
if (drawable != null) {
return drawable;
}
}
//加载策略非空 可以通过加载策略去加载drawable,开发者可自定义
if (mStrategy != null) {
Drawable drawable = mStrategy.getDrawable(context, mSkinName, resId);
if (drawable != null) {
return drawable;
}
}
//非默认皮肤 去皮肤中加载资源
if (!isDefaultSkin) {
//皮肤资源id 这是我们的目标
int targetResId = getTargetResId(context, resId);
if (targetResId != 0) {
//根据id通过皮肤的Resources去获取drawable
return mResources.getDrawable(targetResId);
}
}
return context.getResources().getDrawable(resId);
}
おそらく、キャッシュ リソースがある場合 (スキン パッケージ内のこの resId を持つリソースは以前に取得されている)、キャッシュ リソースが取得され、キャッシュがない場合は、次に従ってスキンのリソースを通じてドローアブルが取得されることを意味します。 resId。
この時点で、スキン パッケージ内のドローアブルが取得されました、つまり、スキン パッケージ内の画像、形状、その他のリソースの動的ロードが実現されました。スキンにカラーをロードするプロセスも同様です。ここでは紹介しませんが、ついに、スキンチェンジ事業が完了しました。
4. 原則を簡単に要約します(この記事の本質)
- APP 内のすべてのアクティビティのライフサイクルを監視します (registerActivityLifecycleCallbacks())
- 各アクティビティの onCreate() メソッドが呼び出されるとき、setFactory() はビューを作成するファクトリを設定し、ビューの作成の雑用を SkinCompatViewInflater に処理します。
- ライブラリ自体はシステム コントロールを書き換え (たとえば、View はライブラリ内の SkinCompatView に対応します)、スキニング インターフェイスを実装し (インターフェイスには applySkin() メソッドが 1 つだけあります)、コントロールがスキニングをサポートしていることを示します。いつでもスキンを変更できるように、後で収集します。
- ライブラリに自分で作成したコントロール内のいくつかの特殊なプロパティ (background、textColor など) を解析し、保存します。
- スキンを切り替えるときは、以前にキャッシュされた View を 1 回走査し、それによって実装されたインターフェイス メソッド applySkin() を呼び出し、applySkin() でスキン リソース (ネットワークまたはローカルから取得したスキン パッケージ) からリソースを取得します。 set コントロールの背景または textColor を変更できます。
オープンソースに感謝します、作者に感謝します。プロジェクトアドレス: https://github.com/ximsfei/Android-skin-support
転載:https://juejin.cn/post/6844903647868878861