Android進(jìn)階Hook攔截系統(tǒng)實例化View過程實現(xiàn)App換膚功能
正文
對于換膚技術(shù),相信伙伴們都見過一些大型app,到了某些節(jié)日或者活動,e.g. 雙十一、雙十二、春節(jié)等等,app的ICON還有內(nèi)部的頁面主題背景都被換成對應(yīng)的皮膚,像這種換膚肯定不是為了某個活動單獨發(fā)一個版本,這樣的話就太雞肋了,很多大廠都有自己的換膚技術(shù),不需要通過發(fā)版就可以實時換膚,活動結(jié)束之后自動下掉,所以有哪些資源可以通過換膚來進(jìn)行切換的呢?
其實在Android的res目錄下所有資源都可以進(jìn)行換膚,像圖片、文字顏色、字體、背景等都可以通過換膚來進(jìn)行無卡頓切換,那么究竟如何才能高效穩(wěn)定地實現(xiàn)換膚,我們需要對于View的生命周期以及加載流程有一定的認(rèn)識。
1 XML布局的解析流程
如果沒有使用Compose,我們現(xiàn)階段的Android開發(fā)布局依然是在XML文件中,如下所示:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="80dp" android:background="#2196F3" android:text="這是頂部TextView" android:gravity="center" android:textColor="#FFFFFF" app:layout_behavior=".behavior.ScrollBehavior"/> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rv_child" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="80dp" app:layout_behavior=".behavior.RecyclerViewBehavior"/> </androidx.coordinatorlayout.widget.CoordinatorLayout>
所以如果想要改變字體顏色,就需要動態(tài)修改textColor屬性;如果想改變背景,就需要修改background屬性;當(dāng)一個Activity想要加載某個布局文件的時候,就需要調(diào)用setContentView方法,實例化View;
setContentView(R.layout.activity_main)
那么我們是否能夠改變系統(tǒng)加載布局文件的邏輯,讓其加載我們自己的皮膚包,那這樣是不是就能夠?qū)崿F(xiàn)動態(tài)換膚?
<>1.1 setContentView源碼分析
我這邊看的是Android 11的源碼,算是比較新的了吧,伙伴們可以跟著看一下。
@Override public void setContentView(@LayoutRes int layoutResID) { initViewTreeOwners(); getDelegate().setContentView(layoutResID); }
一般情況下,我們傳入的就是一個布局id,內(nèi)部實現(xiàn)是調(diào)用了AppCompatDelegate實現(xiàn)類的setContentView方法,AppCompatDelegate是一個抽象類,它的實現(xiàn)類為AppCompatDelegateImpl。
@NonNull public AppCompatDelegate getDelegate() { if (mDelegate == null) { mDelegate = AppCompatDelegate.create(this, this); } return mDelegate; }
所以我們看下AppCompatDelegateImpl的setContentView方法。
@Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent); mAppCompatWindowCallback.bypassOnContentChanged(mWindow.getCallback()); }
首先調(diào)用了ensureSubDecor方法,這里我就不細(xì)說了,這個方法的目的就是保證DecorView創(chuàng)建成功,我們看下這個圖,布局的層級是這樣的。
我們所有的自定義布局,都是加載在DecorView這個容器上,我們看下面這個布局:
<androidx.appcompat.widget.ActionBarOverlayLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/decor_content_parent" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <!--布局id為 action_bar_activity_content----> <include layout="@layout/abc_screen_content_include"/> <androidx.appcompat.widget.ActionBarContainer android:id="@+id/action_bar_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" style="?attr/actionBarStyle" android:touchscreenBlocksFocus="true" android:gravity="top"> <androidx.appcompat.widget.Toolbar android:id="@+id/action_bar" android:layout_width="match_parent" android:layout_height="wrap_content" app:navigationContentDescription="@string/abc_action_bar_up_description" style="?attr/toolbarStyle"/> <androidx.appcompat.widget.ActionBarContextView android:id="@+id/action_context_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" android:theme="?attr/actionModeTheme" style="?attr/actionModeStyle"/> </androidx.appcompat.widget.ActionBarContainer> </androidx.appcompat.widget.ActionBarOverlayLayout>
看布局你可能會覺得,這個是啥?這個是系統(tǒng)appcompat包中的一個布局文件,名字為adb_screen_toolbar.xml,當(dāng)我們新建一個app項目的時候,見到的第一個頁面,如下圖所示
紅框展示的布局就是上面這個XML,也就是DecorView加載的布局文件R.layout.adb_screen_toolbar.xml。
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById( R.id.action_bar_activity_content); final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content); if (windowContentView != null) { // There might be Views already added to the Window's content view so we need to // migrate them to our content view while (windowContentView.getChildCount() > 0) { final View child = windowContentView.getChildAt(0); windowContentView.removeViewAt(0); contentView.addView(child); } // Change our content FrameLayout to use the android.R.id.content id. // Useful for fragments. windowContentView.setId(View.NO_ID); contentView.setId(android.R.id.content); // The decorContent may have a foreground drawable set (windowContentOverlay). // Remove this as we handle it ourselves if (windowContentView instanceof FrameLayout) { ((FrameLayout) windowContentView).setForeground(null); } }
對于DecorView的加載,因為設(shè)置不同主題就會加載不同的XML,這里我不做過多的講解,因為主要目標(biāo)是換膚,但是上面這段代碼需要關(guān)注一下,就是DecorView布局加載出來之后,獲取了include中的id為action_bar_activity_content的容器,將其id替換成了content。
我們再回到setContentView方法中,我們看又是通過mSubDecor獲取到了content這個id對應(yīng)的容器,通過Inflate的形式將我們的布局加載到這個容器當(dāng)中,所以核心點就是Inflate是如何加載并實例化View的。
1.2 LayoutInflater源碼分析
我們換膚的重點就是對于LayoutInflater源碼的分析,尤其是inflate方法,直接返回了一個View。
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) + ")"); } View view = tryInflatePrecompiled(resource, res, root, attachToRoot); if (view != null) { return view; } // 這里是進(jìn)行XML布局解析 XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }
首先是通過XmlParser工具進(jìn)行布局解析,這部分就不講了沒有意義,重點看下面的代碼實現(xiàn):
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate"); final Context inflaterContext = mContext; final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context) mConstructorArgs[0]; mConstructorArgs[0] = inflaterContext; View result = root; try { advanceToRootNode(parser); // 代碼 - 1 final String name = parser.getName(); // ...... 省略部分代碼 if (TAG_MERGE.equals(name)) { if (root == null || !attachToRoot) { throw new InflateException("<merge /> can be used only with a valid " + "ViewGroup root and attachToRoot=true"); } rInflate(parser, root, inflaterContext, attrs, false); } else { // Temp is the root view that was found in the xml // 代碼 - 2 final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; if (root != null) { if (DEBUG) { System.out.println("Creating params from root: " + root); } // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } if (DEBUG) { System.out.println("-----> start inflating children"); } // Inflate all children under temp against its context. rInflateChildren(parser, temp, attrs, true); if (DEBUG) { System.out.println("-----> done inflating children"); } // We are supposed to attach all the views we found (int temp) // to root. Do that now. if (root != null && attachToRoot) { root.addView(temp, params); } // Decide whether to return the root that was passed in or the // top view found in xml. if (root == null || !attachToRoot) { result = temp; } } } return result; } }
伙伴們從代碼中標(biāo)記的tag,自行找對應(yīng)的代碼講解
代碼 - 1
前面我們通過XML布局解析,拿到了布局文件中的信息,這個name其實就是我們在XML中寫的控件的名稱,例如TextView、Button、LinearLayout、include、merge......
如果是merge標(biāo)簽的話,跟其他控件走的渲染方式不一樣,我們重點看 代碼-2 中的實現(xiàn)。
代碼 - 2
這里有一個核心方法,createViewFromTag,最終返回了一個View,這里就包含系統(tǒng)創(chuàng)建并實例化View的秘密。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { if (name.equals("view")) { name = attrs.getAttributeValue(null, "class"); } // Apply a theme wrapper, if allowed and one is specified. if (!ignoreThemeAttr) { final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); final int themeResId = ta.getResourceId(0, 0); if (themeResId != 0) { context = new ContextThemeWrapper(context, themeResId); } ta.recycle(); } try { // 代碼 - 3 View view = tryCreateView(parent, name, context, attrs); // 代碼 - 4 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; } }
代碼 - 3
其實createViewFromTag這個方法中,最終的一個方法就是tryCreateView,在這個方法中返回的View就是createViewFromTag的返回值,當(dāng)然也有可能創(chuàng)建失敗,最終走到 代碼-4中,但我們先看下這個方法。
public final View tryCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { if (name.equals(TAG_1995)) { // Let's party like it's 1995! return new BlinkLayout(context, attrs); } 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; }
在這個方法中,我們看到創(chuàng)建View,其實是通過兩個Factory,分別是:mFactory2和mFactory,通過調(diào)用它們的onCreateView方法進(jìn)行View的實例化,如果這兩個Factory都沒有設(shè)置,那么最終返回的view = null;當(dāng)然后面也有一個兜底策略,如果view = null,但是mPrivateFactory(其實也是Factory2)不為空,也可以通過mPrivateFactory創(chuàng)建。
1.3 Factory接口
在前面我們提到兩個成員變量,分別是:mFactory2和mFactory,這兩個變量是LayoutInflater中的成員變量,我們看下是在setFactory和setFactory2中進(jìn)行賦值的。
public void setFactory(Factory 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 = factory; } else { mFactory = new FactoryMerger(factory, null, mFactory, mFactory2); } } /** * 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); } }
我們系統(tǒng)在進(jìn)行布局解析的時候,肯定也是設(shè)置了自己的Factory,這樣的話就直接走系統(tǒng)的初始化流程;
protected LayoutInflater(LayoutInflater original, Context newContext) { StrictMode.assertConfigurationContext(newContext, "LayoutInflater"); mContext = newContext; mFactory = original.mFactory; mFactory2 = original.mFactory2; mPrivateFactory = original.mPrivateFactory; setFilter(original.mFilter); initPrecompiledViews(); }
但是如果我們想實現(xiàn)換膚,是不是也可自定義換膚的Factory來代替系統(tǒng)的Factory,以此實現(xiàn)我們想要的效果,e.g. 我們在XML布局中設(shè)置了一個TextView
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/cs_root" xmlns:app="http://schemas.android.com/apk/res-auto"> <TextView android:id="@+id/tv_skin" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="開啟換膚" android:textColor="#000000" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> </androidx.constraintlayout.widget.ConstraintLayout>
我們通過自定義的Factory2,在onCreateView中創(chuàng)建一個Button替代TextView。
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { // 代碼 - 5 super.onCreate(savedInstanceState) val inflater = LayoutInflater.from(this) inflater.factory2 = object : LayoutInflater.Factory2 { override fun onCreateView( parent: View?, name: String, context: Context, attrs: AttributeSet ): View? { if (name == "TextView") { val button = Button(context) button.setText("換膚") return button } return null } override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { return null } } val view = inflater.inflate(R.layout.layout_skin, findViewById(R.id.cs_root), false) setContentView(view) }
但是運行之后,我們發(fā)現(xiàn)報錯了:
Caused by: java.lang.IllegalStateException: A factory has already been set on this LayoutInflater
at android.view.LayoutInflater.setFactory2(LayoutInflater.java:314)
at com.lay.learn.asm.MainActivity.onCreate(Unknown Source:22)
看報錯的意思是已經(jīng)設(shè)置了一個factory,不能重復(fù)設(shè)置。這行報錯信息,我們在1.3開頭的代碼中就可以看到,有一個標(biāo)志位mFactorySet,如果mFactorySet = true,那么就直接報錯了,但是在LayoutInflater源碼中,只有在調(diào)用setFactory和setFactory2方法的時候,才會將其設(shè)置為true,那為什么還報錯呢?
代碼 - 5
既然只有在調(diào)用setFactory和setFactory2方法的時候,才會設(shè)置mFactorySet為true,那么原因只會有一個,就是重復(fù)調(diào)用。我們看下super.onCreate(saveInstanceState)做了什么。
因為當(dāng)前Activity繼承了AppCompatActivity,在AppCompatActivity的構(gòu)造方法中調(diào)用了initDelegate方法。
@ContentView public AppCompatActivity(@LayoutRes int contentLayoutId) { super(contentLayoutId); initDelegate(); } private void initDelegate() { // TODO: Directly connect AppCompatDelegate to SavedStateRegistry getSavedStateRegistry().registerSavedStateProvider(DELEGATE_TAG, new SavedStateRegistry.SavedStateProvider() { @NonNull @Override public Bundle saveState() { Bundle outState = new Bundle(); getDelegate().onSaveInstanceState(outState); return outState; } }); addOnContextAvailableListener(new OnContextAvailableListener() { @Override public void onContextAvailable(@NonNull Context context) { final AppCompatDelegate delegate = getDelegate(); delegate.installViewFactory(); delegate.onCreate(getSavedStateRegistry() .consumeRestoredStateForKey(DELEGATE_TAG)); } }); }
最終會調(diào)用AppCompatDelegateImpl的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獲取到factory為空,那么就會調(diào)用setFactory2方法,這個時候mFactorySet = true,當(dāng)我們再次調(diào)用setContentView的時候,就直接報錯,所以我們需要在super.onCreate之前進(jìn)行換膚的操作。
當(dāng)然我們也可以通過反射的方式,在setFactory的時候?qū)FactorySet設(shè)置為false。
1.4 小結(jié)
所以最終換膚的方案:通過Hook的形式,修改替代系統(tǒng)的Factory,從而自行完成組件的實例化,達(dá)到與系統(tǒng)行為一致的效果。
代碼 - 4
如果有些View通過Factory沒有實例化的,此時view為空,那么會通過反射的方式來完成組件實例化,像一些帶包名的系統(tǒng)組件,或者自定義View。
2 換膚框架搭建
其實在搭建換膚框架的時候,我們肯定不可能對所有的控件都進(jìn)行換膚,所以對于XML布局中的組件,我們需要進(jìn)行一次標(biāo)記,那么標(biāo)記的手段有哪些呢?
(1)創(chuàng)建一個接口,e.g. ISkinChange接口,然后重寫系統(tǒng)所有需要換膚的控件實現(xiàn)這個接口,然后遍歷獲取XML中需要換膚的控件,進(jìn)行換膚,這個是一個方案,但是成本比較高。
(2)自定義屬性,因為對于每個控件來說都有各自的屬性,如果我們通過自定義屬性的方式給每個需要換膚的控件加上這個屬性,在實例化View的時候就可以進(jìn)行區(qū)分。
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="Skinable"> <attr name="isSupport" format="boolean"/> </declare-styleable> </resources>
第一步:創(chuàng)建View并返回
這里我們創(chuàng)建了一個SkinFactory,實現(xiàn)了LayoutInflater.Factory2接口,這個類就是用于收集需要換膚的組件,并實現(xiàn)換膚的功能。
class SkinFactory : LayoutInflater.Factory2 { override fun onCreateView( parent: View?, name: String, context: Context, attrs: AttributeSet ): View? { //創(chuàng)建View //收集可以換膚的組件 return null } override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { return null } }
首先在onCreateView中,需要創(chuàng)建一個View并返回,我們看下系統(tǒng)是怎么完成的。
通過上面的截圖我們知道,通過AppCompatDelegate的實現(xiàn)類就能夠?qū)崿F(xiàn)view的創(chuàng)建。
override fun onCreateView( parent: View?, name: String, context: Context, attrs: AttributeSet ): View? { //創(chuàng)建View val view = delegate.createView(parent, name, context, attrs) if (view == null) { //TODO 沒有創(chuàng)建成功,需要通過反射來創(chuàng)建 } //收集可以換膚的組件 if (view != null) { collectSkinComponent(attrs, context, view) } return view } /** * 收集能夠進(jìn)行換膚的控件 */ private fun collectSkinComponent(attrs: AttributeSet, context: Context, view: View) { //獲取屬性 val skinAbleAttr = context.obtainStyledAttributes(attrs, R.styleable.Skinable, 0, 0) val isSupportSkin = skinAbleAttr.getBoolean(R.styleable.Skinable_isSupport, false) if (isSupportSkin) { val attrsMap: MutableMap<String, String> = mutableMapOf() //收集起來 for (index in 0 until attrs.attributeCount) { val name = attrs.getAttributeName(index) val value = attrs.getAttributeValue(index) attrsMap[name] = value } val skinView = SkinView(view, attrsMap) skinList.add(skinView) } skinAbleAttr.recycle() }
所以我們在SkinFactory中傳入一個AppCompatDelegate的實現(xiàn)類,調(diào)用createView方法先創(chuàng)建一個View,如果這個view不為空,那么會收集每個View的屬性,看是否支持換膚。
收集能夠換膚的組件,其實就是根據(jù)自定義屬性劃分,通過獲取View中自帶全部屬性判斷,如果支持換膚,那么就存儲起來,這部分還是比較簡單的。
第二步:換膚邏輯與Activity基類抽取
如果我們想要進(jìn)行換膚,例如更換背景、或者更換字體顏色等等,因此我們需要設(shè)置幾個換膚的類型如下:
sealed class SkinType{ /** * 更換背景顏色 * @param color 背景顏色 */ class BackgroundSkin(val color:Int):SkinType() /** * 更換背景圖片 * @param drawable 背景圖片資源id */ class BackgroundDrawableSkin(val drawable:Int):SkinType() /** * 更換字體顏色 * @param color 字體顏色 * NOTE 這個只能TextView才能是用 */ class TextColorSkin(val color: Int):SkinType() /** * 更換字體類型 * @param textStyle 字體型號 * NOTE 這個只能TextView才能是用 */ class TextStyleSkin(val textStyle: Typeface):SkinType() }
當(dāng)開啟換膚之后,需要遍歷skinList中支持換膚的控件,然后根據(jù)SkinType來對對應(yīng)的控件設(shè)置屬性,例如TextStyleSkin這類換膚類型,只能對TextView生效,因此需要根據(jù)view的類型來進(jìn)行屬性設(shè)置。
/** * 一鍵換膚 */ fun changedSkin(vararg skinType: SkinType) { Log.e("TAG","skinList $skinList") skinList.forEach { skinView -> changedSkinInner(skinView, skinType) } } /** * 換膚的內(nèi)部實現(xiàn)類 */ private fun changedSkinInner(skinView: SkinView, skinType: Array<out SkinType>) { skinType.forEach { type -> Log.e("TAG", "changedSkinInner $type") when (type) { is SkinType.BackgroundSkin -> { skinView.view.setBackgroundColor(type.color) } is SkinType.BackgroundDrawableSkin -> { skinView.view.setBackgroundResource(type.drawable) } is SkinType.TextStyleSkin -> { if (skinView.view is TextView) { //只有TextView可以換 skinView.view.typeface = type.textStyle } } is SkinType.TextColorSkin -> { if (skinView.view is TextView) { //只有TextView可以換 skinView.view.setTextColor(type.color) } } } } }
所以針對換膚的需求,我們可以抽出一個抽象的Activity基類,叫做SkinActivity。
abstract class SkinActivity : AppCompatActivity() { private lateinit var skinFactory: SkinFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.e("TAG", "onCreate") val inflate = LayoutInflater.from(this) //恢復(fù)標(biāo)志位 resetmFactorySet(inflate) //開啟換膚模式 skinFactory = SkinFactory(delegate) inflate.factory2 = skinFactory setContentView(inflate.inflate(getLayoutId(), getViewRoot(), false)) initView() } open fun initView() { } protected fun changedSkin(vararg skinType: SkinType) { Log.e("TAG", "changedSkin") skinFactory.changedSkin(*skinType) } @SuppressLint("SoonBlockedPrivateApi") private fun resetmFactorySet(instance: LayoutInflater) { val mFactorySetField = LayoutInflater::class.java.getDeclaredField("mFactorySet") mFactorySetField.isAccessible = true mFactorySetField.set(instance, false) } abstract fun getLayoutId(): Int abstract fun getViewRoot(): ViewGroup? }
在onCreate方法中,主要就是進(jìn)行Factory的設(shè)置,這里就是我們前面提到的SkinFactory(實現(xiàn)了Factory2接口),然后定義了一個方法changedSkin,在任意子類中都可以調(diào)用。
class SkinChangeActivity : SkinActivity() { override fun initView() { findViewById<Button>(R.id.btn_skin).setOnClickListener { Toast.makeText(this,"更換背景",Toast.LENGTH_SHORT).show() changedSkin( SkinType.BackgroundSkin(Color.parseColor("#B81A1A")) ) } findViewById<Button>(R.id.btn_skin_textColor).setOnClickListener { Toast.makeText(this,"更換字體顏色",Toast.LENGTH_SHORT).show() changedSkin( SkinType.TextColorSkin(Color.parseColor("#FFEB3B")), SkinType.BackgroundSkin(Color.WHITE) ) } findViewById<Button>(R.id.btn_skin_textStyle).setOnClickListener { Toast.makeText(this,"更換字體樣式",Toast.LENGTH_SHORT).show() changedSkin( SkinType.TextStyleSkin(Typeface.DEFAULT_BOLD), ) } } override fun getLayoutId(): Int { return R.layout.activity_skin_change } override fun getViewRoot(): ViewGroup? { return findViewById(R.id.cs_root) } }
具體的效果可以看動圖:
其實這里只是實現(xiàn)了一個簡單的換膚效果,其實在業(yè)務(wù)代碼中,可能存在上千個View,那么通過這種方式就能夠避免給每個View都去設(shè)置一遍背景、字體顏色......關(guān)鍵還是在于原理的理解,其實真正的換膚現(xiàn)在主流的都是插件化換膚,通過下載皮膚包自動配置到App中,后續(xù)我們就會介紹插件化換膚的核心思想。
以上就是Android進(jìn)階Hook攔截系統(tǒng)實例化View過程實現(xiàn)App換膚功能的詳細(xì)內(nèi)容,更多關(guān)于Android Hook攔截View換膚的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android在一個app中安裝并卸載另一個app的示例代碼
這篇文章主要介紹了Android在一個app中安裝并卸載另一個app的示例代碼,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03android獲取附近藍(lán)牙設(shè)備并計算距離的實例代碼
下面小編就為大家分享一篇android獲取附近藍(lán)牙設(shè)備并計算距離的實例代碼,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01Android實現(xiàn)歌曲播放時歌詞同步顯示具體思路
歌曲播放時歌詞同步顯示,我們需要讀取以上歌詞文件的每一行轉(zhuǎn)換成成一個個歌詞實體,可根據(jù)當(dāng)前播放器的播放進(jìn)度與每句歌詞的開始時間,得到當(dāng)前屏幕中央高亮顯示的那句歌詞2013-06-06Android打造屬于自己的新聞平臺(客戶端+服務(wù)器)
這篇文章主要為大家詳細(xì)介紹了Android打造屬于自己的新聞平臺的相關(guān)資料,Android實現(xiàn)新聞客戶端服務(wù)器,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-06-06Android給scrollView截圖超過屏幕大小形成長圖
這篇文章主要為大家詳細(xì)介紹了Android給scrollView截圖超過屏幕大小形成長圖,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12Android開發(fā)自學(xué)筆記(一):Hello,world!
這篇文章主要介紹了Android開發(fā)自學(xué)筆記(一):Hello,world!本文講解了創(chuàng)建HelloWorld工程、編寫代碼、啟動模擬器等步驟,需要的朋友可以參考下2015-04-04