Android插件化换肤
Android插件化換膚
前言(廢話)
今年是大年三十,今年怎么說呢,總體還是讓自己感覺到比較滿意的,但是有些時(shí)候還是感覺自己的自覺性不夠。先賢曾經(jīng)說過,君子慎獨(dú),愿明年的我能夠銘記于心。
我這輩子最崇拜的人或許就是張載了,僅僅因?yàn)樗臋M渠四句:為天地立心,為生民立命,為往圣繼絕學(xué),為天下開太平!
思維導(dǎo)圖
正文
概要
這個(gè)屬于老生常談的問題了。比如說,一旦出現(xiàn)了什么非常令人悲痛的事件,每個(gè)應(yīng)用都會(huì)把自己的主色調(diào)改成黑色,抑或是應(yīng)用內(nèi)提供給用戶自主選擇(像網(wǎng)易云音樂),選擇自己喜歡的主題色,再比如,在特定的情況下,希望附加一些新的主題讓用戶來選擇和使用。
互聯(lián)網(wǎng)公司,一般都會(huì)折騰來折騰去,用來表示自己其實(shí)一直在創(chuàng)新,所以常常會(huì)有這樣神奇的需求,但是我總不可能對于每一個(gè)顯示的視圖,都寫一連串的if-else主題判斷邏輯,然后每次切換主題的時(shí)候都按照這個(gè)邏輯去逐一切換主題,這樣做是不是有點(diǎn)太傻了。最大的問題就是影響過大,即任意一個(gè)新創(chuàng)建的布局信息都需要去重新調(diào)用這樣一個(gè)方法。打一個(gè)比方,我僅僅是有一顆蛀牙,你卻直接給我做了一整套整形手術(shù),割雞用牛刀,雖然問題也解決了,未免過于大材小用。
常規(guī)的僅僅面向業(yè)務(wù)的開發(fā)過程中,僅僅指定布局即直接調(diào)用setContentView()方法就能繪制完整的一個(gè)界面。就好比我自己有時(shí)候研究一些功能的時(shí)候,往往都會(huì)直接一行代碼就解決一個(gè)空Activity的定義,如下。
class MainActivity : BaseActivity(R.layout.activity_main)我們都知道,通過xml來進(jìn)行繪制的視圖都是通過LayoutInflater間接調(diào)用對應(yīng)視圖類的兩個(gè)參數(shù)的構(gòu)造方法進(jìn)行的。所以相對理想的解決方案就是反射這兩個(gè)方法來達(dá)成我們的目的。
既然要進(jìn)行反射修改對應(yīng)的操作邏輯,還是從setContentView()方法開始吧。
當(dāng)你不斷追溯創(chuàng)建試圖的源頭,到最后一定會(huì)追溯到LayoutInflater的這么一個(gè)方法
public final View createView(@NonNull Context viewContext, @NonNull String name,@Nullable String prefix, @Nullable AttributeSet attrs)throws ClassNotFoundException, InflateException {在這個(gè)方法里會(huì)進(jìn)行反射,從而根據(jù)xml中的標(biāo)簽調(diào)用相應(yīng)視圖的兩個(gè)參數(shù)的構(gòu)造方法。
constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor);而這個(gè)mConstructorSignature則是作為常量定義在LayoutInflater類中的。
static final Class<?>[] mConstructorSignature = new Class[] {Context.class, AttributeSet.class};好了,接下來看看LayoutInflater中的inflate方法
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;}XmlResourceParser parser = res.getLayout(resource);try {return inflate(parser, root, attachToRoot);} finally {parser.close();}}該方法間接調(diào)用了createViewFromTag(View parent, String name, Context context, AttributeSet attrs),可以看到該方法間接調(diào)用了該方法同名的五個(gè)參數(shù)方法。
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {return createViewFromTag(parent, name, context, attrs, false); } 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 {View view = tryCreateView(parent, name, context, attrs);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;} catch (InflateException e) {throw e;} catch (ClassNotFoundException e) {final InflateException ie = new InflateException(getParserStateDescription(context, attrs)+ ": Error inflating class " + name, e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} catch (Exception e) {final InflateException ie = new InflateException(getParserStateDescription(context, attrs)+ ": Error inflating class " + name, e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} }我們重點(diǎn)看看View view = tryCreateView(parent, name, context, attrs);這行方法,這行代碼的邏輯是允許通過一種自定義的方法來人為生成新的視圖,因而我們完全可以基于這點(diǎn)來添加我們自己的視圖生成邏輯,如何生成我們將在稍后講到。我們可以看到,可以依次通過factory2和factory以及mPrivateFactory來嘗試生成視圖(tryCreateView)。
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;}對于這個(gè)參數(shù),我們可以通過setFactory2(Factory2 factory)方法來賦予自定義視圖生成邏輯,估計(jì)之前這個(gè)參數(shù)是私有的,只能通過反射來獲取。但是這些洋鬼子猛然發(fā)現(xiàn),這種需求他們也需要用到,所以他們就把這個(gè)參數(shù)放出來,也方便自己調(diào)用。
整體思路
首先,Application本身對于Activity其實(shí)有著一定的監(jiān)控,雖然其實(shí)實(shí)際上這個(gè)類僅僅是一個(gè)上下文而已,實(shí)際上的操作邏輯還是在儀表盤Instrumentation上。Application類提供一個(gè)方法android.app.Application#registerActivityLifecycleCallbacks,用來設(shè)置各個(gè)Activity的回調(diào)。我們調(diào)用setContentView()方法是在Activity的onCreate()方法中進(jìn)行的,因?yàn)樵谡{(diào)用這個(gè)方法之后,視圖才生成完畢,所以如果我們想要對每個(gè)Activity的視圖繪制動(dòng)手腳,我們還是需要對onCreate環(huán)節(jié)做文章。大致的想法就是基于觀察者模式將主題切換封裝成一個(gè)Observable,然后讓每個(gè)Activity在onCreate階段來主動(dòng)地訂閱它。這樣以來,當(dāng)我們改變被觀察者的狀態(tài)時(shí),觀察者也就是每個(gè)Activity就可以自動(dòng)的來根據(jù)新的主題來作出改變。
具體實(shí)現(xiàn)
定義一個(gè)SkinManager類
該類用以統(tǒng)一管理皮膚的設(shè)置以及還原。Linux的設(shè)計(jì)核心思想就是一切都是文件,順便說一句,java虛擬機(jī)的設(shè)計(jì)思想是一切都是對象。所以,很多時(shí)候,我們在開發(fā)的過程都能發(fā)現(xiàn),幾乎我們所操作的一切都是直接或者間接地在和文件打交道。
在這里,我們所設(shè)計(jì)的SkinManager類也類似,我們只需要直接提供皮膚插件包的文件路徑(這里注意在不同的android版本中,對于系統(tǒng)路徑的訪問控制是不同的,所以皮膚插件包的路徑還是需要好好考慮清楚,估計(jì)也是為了很多安全性的考慮),我最先能想到的就是當(dāng)時(shí)做應(yīng)用的在線更新時(shí),不同版本的應(yīng)用安全真的讓我感覺一直在繞,6.0的權(quán)限,7.0的contentProvider,8.0的未知來源確認(rèn)。
class SkinManager(private val mApplication: Application) : Observable() {private val skinActivityLifecycle: ApplicationActivityLifeCycle/*** SkinManager初始化時(shí),初始化SkinPreference和SkinResource*/init {SkinPreference.init(mApplication)SkinResources.init(mApplication)//創(chuàng)建Application回調(diào)skinActivityLifecycle = ApplicationActivityLifeCycle(this)//注冊Application執(zhí)行Activity各個(gè)流程的回調(diào)mApplication.registerActivityLifecycleCallbacks(skinActivityLifecycle)loadSkin(SkinPreference.instance().getSkin())}companion object {@Volatilevar instance: SkinManager? = nullfun instance(): SkinManager = instance ?: error("call getInstance(Application) first!")fun getInstance(mApplication: Application) =if (instance == null) {synchronized(SkinManager::class.java) {if (instance == null) instance = SkinManager(mApplication)}} else null}fun loadSkin(skinPath: String?) {when {TextUtils.isEmpty(skinPath) -> {SkinPreference.instance().reset()SkinResources.instance().reset()}else -> {val appResources = mApplication.resourcesval assetManager = AssetManager::class.java.newInstance()val addAssetPath = AssetManager::class.java.getMethod(Method.AssetManager_addAssetPath, String::class.java)addAssetPath.invoke(assetManager, skinPath!!)val skinResources = Resources(assetManager, appResources.displayMetrics, appResources.configuration)val mPm = mApplication.packageManagerval info = mPm.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES)SkinResources.instance().applySkin(skinResources, info?.packageName)SkinPreference.instance().setSkin(skinPath)}}setChanged()notifyObservers(null)}}如上述代碼,當(dāng)我們從指定路徑加載了對應(yīng)的皮膚插件包后,我們將這些皮膚信息存儲(chǔ)到SkinResources類中,并且通知訂閱者來根據(jù)插件包中的信息來更新其中的對應(yīng)視圖的信息。
class SkinResources(context: Context) {private val mAppResources: Resources = context.resourcesprivate var mSkinResources: Resources? = nullprivate var mSkinPkgName: String? = ""private var isDefaultSkin = truecompanion object {private var instance: SkinResources? = nullfun instance() = instance!!fun init(context: Context) {if (instance == null)synchronized(SkinResources::class) {if (instance == null)instance = SkinResources(context)}}}fun reset() {mSkinResources = nullmSkinPkgName = ""isDefaultSkin = true}fun applySkin(resources: Resources?, pkgName: String?) {mSkinResources = resourcesmSkinPkgName = pkgNameisDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null}private fun getIdentifier(resId: Int) = if (isDefaultSkin) resIdelse mSkinResources?.getIdentifier(mAppResources.getResourceEntryName(resId),mAppResources.getResourceTypeName(resId), mSkinPkgName)fun getColor(resId: Int): Int = when {isDefaultSkin -> mAppResources.getColor(resId)getIdentifier(resId) == 0 -> mAppResources.getColor(resId)else -> mSkinResources!!.getColor(getIdentifier(resId)!!)}fun getColorStateList(resId: Int) = when {isDefaultSkin -> mAppResources.getColorStateList(resId)getIdentifier(resId) == 0 -> mAppResources.getColorStateList(resId)else -> mSkinResources!!.getColorStateList(getIdentifier(resId)!!)}fun getDrawable(resId: Int) = when {isDefaultSkin -> mAppResources.getDrawable(resId)resId.identifier == 0 -> mAppResources.getDrawable(resId)else -> mSkinResources?.getDrawable(resId.identifier)}fun getBackground(resId: Int): Any = when (mAppResources.getResourceTypeName(resId)) {"color" -> getColor(resId)else -> getDrawable(resId)!!}private val Int.identifier: Intget() = getIdentifier(this)!! }實(shí)現(xiàn)ActivityLifecycleCallbacks接口
也就是說我們需要自定義一個(gè)類來實(shí)現(xiàn)ActivityLifecycleCallbacks接口(顧名思義,這個(gè)接口就是對于activity的各個(gè)流程的回調(diào))。我們在前面說過,只需要設(shè)置自定義的Factory2類就能讓activity來以更高的優(yōu)先級來使用我們所設(shè)置的自定義的視圖繪制邏輯。所以實(shí)現(xiàn)對應(yīng)接口的邏輯如下,順便說一句,我是代碼精簡主義者,另外我特別討厭多層大括號的嵌套{},所以很多時(shí)候,kotlin原生支持的函數(shù)式編程,我非常中意!
class ApplicationActivityLifeCycle(var observable: Observable) :Application.ActivityLifecycleCallbacks {private val mLayoutInflaterFactories by lazy { ArrayMap<Activity, SkinLayoutInflaterFactory>() }override fun onActivitySaveInstanceState(tActivity: Activity, tBundle: Bundle) = Unitoverride fun onActivityCreated(tActivity: Activity, savedInstanceState: Bundle?) {//更新狀態(tài)欄顏色SkinThemeUtils.updateStatusBarColor(tActivity)val layoutInflater = tActivity.layoutInflater//反射來將該標(biāo)記來設(shè)置為false,因?yàn)樵赾reateViewByTag方法中,只有這個(gè)參數(shù)為false,才會(huì)嘗試基于//factory2來生成視圖layoutInflater.staticSet(LayoutInflater::class.java, Field.LayoutInflater_mFactorySet, false)val skinLayoutInflaterFactory = SkinLayoutInflaterFactory(tActivity)//設(shè)置activity的layoutInlfater中的factory2字段,該靜態(tài)方法對于不同版本有著兼容的效果LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory)mLayoutInflaterFactories[tActivity] = skinLayoutInflaterFactory//設(shè)置觀察者訂閱被觀察者observable.addObserver(skinLayoutInflaterFactory)}override fun onActivityStarted(tActivity: Activity) = Unitoverride fun onActivityResumed(tActivity: Activity) = Unitoverride fun onActivityPaused(tActivity: Activity) = Unitoverride fun onActivityStopped(tActivity: Activity) = Unit//取消訂閱override fun onActivityDestroyed(tActivity: Activity) = observable.deleteObserver(mLayoutInflaterFactories.remove(tActivity)) }我們可以看見,類中通過mLayoutInflaterFactories字段存儲(chǔ)了activity和對應(yīng)的layout2的對應(yīng)關(guān)系,這是便于后續(xù)的進(jìn)一步拓展作著準(zhǔn)備,在這里其實(shí)僅僅需要設(shè)置訂閱以及被訂閱的關(guān)系就行了。
好了,到了最關(guān)鍵的部分了,我們?nèi)绾沃貙慒actory2,我們知道,activity和Factory2是一對一的對應(yīng)關(guān)系,而且我們需要在該類中實(shí)現(xiàn)新建視圖邏輯,并且在里面加上我們所需的更改皮膚邏輯。
雖然說說是更改皮膚,其實(shí)無非是設(shè)置一些視圖的圖片,或者是顏色。
class SkinLayoutInflaterFactory(val activity: Activity) :LayoutInflater.Factory2,Observer {private val skinAttribute by lazy { SkinAttribute() }companion object {val mClassPrefixList = arrayListOf("android.widget.","android.webkit.","android.app.","android.view.")val mConstructorMap = hashMapOf<String, Constructor<out View>>()private val mConstructorSignature = arrayOf(Context::class.java, AttributeSet::class.java)}override fun onCreateView(parent: View?, name: String,context: Context,attrs: AttributeSet) =createSDKView(name, context, attrs).createIfNull { createView(name, context, attrs) }.doIfNotNull { skinAttribute.look(it!!, attrs) }private fun createSDKView(name: String,context: Context,attrs: AttributeSet): View? =if (name.contains('.')) nullelse mClassPrefixList.mapNotNull { createView(it + name, context, attrs) }.getOrNull(0)private fun createView(name: String,context: Context,attrs: AttributeSet): View? =findConstructor(context, name)?.newInstance(context, attrs)private fun findConstructor(context: Context,name: String): Constructor<out View>? {when (mConstructorMap[name]) {null -> try {mConstructorMap[name] =context.classLoader.loadClass(name).asSubclass(View::class.java).getConstructor(*mConstructorSignature)} catch (e: Exception) {LogUtil.e("constructor not found")}}return mConstructorMap[name]}/*** 觀察者模式接收到被觀察者更新的回調(diào)*/override fun update(p0: Observable?, p1: Any?) {SkinThemeUtils.updateStatusBarColor(activity)//更新皮膚中的信息skinAttribute.applySkin()}override fun onCreateView(p0: String, p1: Context, p2: AttributeSet): Nothing? = null }可以操作的視圖分為兩種,一種是官方的一些視圖,對于這些視圖,我們的可控性其實(shí)非常高,只需要對于特定的一些屬性進(jìn)行控制就行了。但是一些我們自定義的視圖,如果也按照這樣的邏輯進(jìn)行篩選的話,可能會(huì)使邏輯過于復(fù)雜和冗余,所以這些視圖我們就特事特辦,讓他們繼承SkinViewSupport接口加以區(qū)分即可。
class SkinAttribute {companion object {//設(shè)置我們所感興趣的屬性val mAttributes = arrayListOf("background","src","textColor","drawableLeft","drawableTop","drawableRight","drawableBottom")}private val mSkinViews = arrayListOf<SkinView>()/*** 我們在factory2的onCreateView方法中添加了對于所創(chuàng)建的每一個(gè)視圖* @see com.ciruy.onion_plugin.SkinLayoutInflaterFactory.onCreateView(android.view.View, java.lang.String, android.content.Context, android.util.AttributeSet)* 可以操作的視圖分為兩種,一種是官方的一些視圖,對于這些視圖,我們的可控性其實(shí)非常高,只需要對于特定的一些屬性進(jìn)行控制就行了* 但是一些我們自定義的視圖,如果也按照這樣的邏輯進(jìn)行篩選的話,可能會(huì)使邏輯過于復(fù)雜和冗余,所以這些視圖我們就特事特辦,* 讓他們繼承SkinViewSupport接口加以區(qū)分即可*/fun look(view: View, attrs: AttributeSet) {val mSkinPairs = arrayListOf<SkinPair>()Array<Pair<String, String>>(attrs.attributeCount) {Pair(attrs.getAttributeName(it), attrs.getAttributeValue(it))}.filter {//如果屬性是我們所感興趣的屬性,并且并非通過硬編碼指定,執(zhí)行后續(xù)操作mAttributes.contains(it.first) && !it.second.startsWith("#")}.map {//pair的首位是屬性的名稱,次位是SkinPair(it.first, when {//可能是直接調(diào)用系統(tǒng)直接提供的顏色或者是圖片,android:background="?android:attr/windowBackground"it.second.startsWith("?") -> SkinThemeUtils.getResId(view.context,intArrayOf(it.second.substring(1).toInt()))[0]//或者是封裝好的顏色以及圖片信息else -> it.second.substring(1).toInt()})}.onEach {mSkinPairs.add(it)}if (mSkinPairs.isNotEmpty() || view is SkinViewSupport) {val skinView = SkinView(view, mSkinPairs)skinView.applySkin()mSkinViews.add(skinView)}}fun applySkin() {mSkinViews.onEach { it.applySkin() }}}class SkinView(private val view: View, private val skinPairs: List<SkinPair>) {fun applySkin() {applySkinSupport()skinPairs.onEach {var left: Drawable? = nullvar top: Drawable? = nullvar right: Drawable? = nullvar bottom: Drawable? = nullwhen (it.attributeName) {"background" -> when (val background = SkinResources.instance().getBackground(it.resId)) {is Int -> view.setBackgroundColor(background)is Drawable ->ViewCompat.setBackground(view, background)else -> throw IllegalStateException("wrong SkinView background class type")}"src" -> when (val background = SkinResources.instance().getBackground(it.resId)) {is Int -> view.asImageView().setImageDrawable(ColorDrawable(background))is Drawable -> view.asImageView().setImageDrawable(background)else -> throw java.lang.IllegalStateException("wrong SkinView background class Type")}"textColor" -> view.asTextView().setTextColor(SkinResources.instance().getColorStateList(it.resId))"drawableLeft" -> left = SkinResources.instance().getDrawable(it.resId)"drawableTop" -> top = SkinResources.instance().getDrawable(it.resId)"drawableRight" -> right = SkinResources.instance().getDrawable(it.resId)"drawableBottom" -> bottom = SkinResources.instance().getDrawable(it.resId)}if (left != null || right != null || top != null || bottom != null)view.asTextView().setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom)}}private fun applySkinSupport() = if (view is SkinViewSupport) view.applySkin() else Unit }data class SkinPair(val attributeName: String, val resId: Int)總結(jié)
以上是生活随笔為你收集整理的Android插件化换肤的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SimCSE论文及源码解读
- 下一篇: 解决Win2003 IIS不能下载rmv