日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > Android >内容正文

Android

Android插件化换肤

發布時間:2023/12/8 Android 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Android插件化换肤 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Android插件化換膚

前言(廢話)

今年是大年三十,今年怎么說呢,總體還是讓自己感覺到比較滿意的,但是有些時候還是感覺自己的自覺性不夠。先賢曾經說過,君子慎獨,愿明年的我能夠銘記于心。

我這輩子最崇拜的人或許就是張載了,僅僅因為他的橫渠四句:為天地立心,為生民立命,為往圣繼絕學,為天下開太平!


思維導圖


正文

概要

這個屬于老生常談的問題了。比如說,一旦出現了什么非常令人悲痛的事件,每個應用都會把自己的主色調改成黑色,抑或是應用內提供給用戶自主選擇(像網易云音樂),選擇自己喜歡的主題色,再比如,在特定的情況下,希望附加一些新的主題讓用戶來選擇和使用。

互聯網公司,一般都會折騰來折騰去,用來表示自己其實一直在創新,所以常常會有這樣神奇的需求,但是我總不可能對于每一個顯示的視圖,都寫一連串的if-else主題判斷邏輯,然后每次切換主題的時候都按照這個邏輯去逐一切換主題,這樣做是不是有點太傻了。最大的問題就是影響過大,即任意一個新創建的布局信息都需要去重新調用這樣一個方法。打一個比方,我僅僅是有一顆蛀牙,你卻直接給我做了一整套整形手術,割雞用牛刀,雖然問題也解決了,未免過于大材小用。

常規的僅僅面向業務的開發過程中,僅僅指定布局即直接調用setContentView()方法就能繪制完整的一個界面。就好比我自己有時候研究一些功能的時候,往往都會直接一行代碼就解決一個空Activity的定義,如下。

class MainActivity : BaseActivity(R.layout.activity_main)

我們都知道,通過xml來進行繪制的視圖都是通過LayoutInflater間接調用對應視圖類的兩個參數的構造方法進行的。所以相對理想的解決方案就是反射這兩個方法來達成我們的目的。

既然要進行反射修改對應的操作邏輯,還是從setContentView()方法開始吧。

當你不斷追溯創建試圖的源頭,到最后一定會追溯到LayoutInflater的這么一個方法

public final View createView(@NonNull Context viewContext, @NonNull String name,@Nullable String prefix, @Nullable AttributeSet attrs)throws ClassNotFoundException, InflateException {

在這個方法里會進行反射,從而根據xml中的標簽調用相應視圖的兩個參數的構造方法。

constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); sConstructorMap.put(name, constructor);

而這個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();}}

該方法間接調用了createViewFromTag(View parent, String name, Context context, AttributeSet attrs),可以看到該方法間接調用了該方法同名的五個參數方法。

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;} }

我們重點看看View view = tryCreateView(parent, name, context, attrs);這行方法,這行代碼的邏輯是允許通過一種自定義的方法來人為生成新的視圖,因而我們完全可以基于這點來添加我們自己的視圖生成邏輯,如何生成我們將在稍后講到。我們可以看到,可以依次通過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;}

對于這個參數,我們可以通過setFactory2(Factory2 factory)方法來賦予自定義視圖生成邏輯,估計之前這個參數是私有的,只能通過反射來獲取。但是這些洋鬼子猛然發現,這種需求他們也需要用到,所以他們就把這個參數放出來,也方便自己調用。

整體思路

首先,Application本身對于Activity其實有著一定的監控,雖然其實實際上這個類僅僅是一個上下文而已,實際上的操作邏輯還是在儀表盤Instrumentation上。Application類提供一個方法android.app.Application#registerActivityLifecycleCallbacks,用來設置各個Activity的回調。我們調用setContentView()方法是在Activity的onCreate()方法中進行的,因為在調用這個方法之后,視圖才生成完畢,所以如果我們想要對每個Activity的視圖繪制動手腳,我們還是需要對onCreate環節做文章。大致的想法就是基于觀察者模式將主題切換封裝成一個Observable,然后讓每個Activity在onCreate階段來主動地訂閱它。這樣以來,當我們改變被觀察者的狀態時,觀察者也就是每個Activity就可以自動的來根據新的主題來作出改變。

具體實現

定義一個SkinManager類

該類用以統一管理皮膚的設置以及還原。Linux的設計核心思想就是一切都是文件,順便說一句,java虛擬機的設計思想是一切都是對象。所以,很多時候,我們在開發的過程都能發現,幾乎我們所操作的一切都是直接或者間接地在和文件打交道。

在這里,我們所設計的SkinManager類也類似,我們只需要直接提供皮膚插件包的文件路徑(這里注意在不同的android版本中,對于系統路徑的訪問控制是不同的,所以皮膚插件包的路徑還是需要好好考慮清楚,估計也是為了很多安全性的考慮),我最先能想到的就是當時做應用的在線更新時,不同版本的應用安全真的讓我感覺一直在繞,6.0的權限,7.0的contentProvider,8.0的未知來源確認。

class SkinManager(private val mApplication: Application) : Observable() {private val skinActivityLifecycle: ApplicationActivityLifeCycle/*** SkinManager初始化時,初始化SkinPreference和SkinResource*/init {SkinPreference.init(mApplication)SkinResources.init(mApplication)//創建Application回調skinActivityLifecycle = ApplicationActivityLifeCycle(this)//注冊Application執行Activity各個流程的回調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)}}

如上述代碼,當我們從指定路徑加載了對應的皮膚插件包后,我們將這些皮膚信息存儲到SkinResources類中,并且通知訂閱者來根據插件包中的信息來更新其中的對應視圖的信息。

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)!! }

實現ActivityLifecycleCallbacks接口

也就是說我們需要自定義一個類來實現ActivityLifecycleCallbacks接口(顧名思義,這個接口就是對于activity的各個流程的回調)。我們在前面說過,只需要設置自定義的Factory2類就能讓activity來以更高的優先級來使用我們所設置的自定義的視圖繪制邏輯。所以實現對應接口的邏輯如下,順便說一句,我是代碼精簡主義者,另外我特別討厭多層大括號的嵌套{},所以很多時候,kotlin原生支持的函數式編程,我非常中意!

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?) {//更新狀態欄顏色SkinThemeUtils.updateStatusBarColor(tActivity)val layoutInflater = tActivity.layoutInflater//反射來將該標記來設置為false,因為在createViewByTag方法中,只有這個參數為false,才會嘗試基于//factory2來生成視圖layoutInflater.staticSet(LayoutInflater::class.java, Field.LayoutInflater_mFactorySet, false)val skinLayoutInflaterFactory = SkinLayoutInflaterFactory(tActivity)//設置activity的layoutInlfater中的factory2字段,該靜態方法對于不同版本有著兼容的效果LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutInflaterFactory)mLayoutInflaterFactories[tActivity] = skinLayoutInflaterFactory//設置觀察者訂閱被觀察者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字段存儲了activity和對應的layout2的對應關系,這是便于后續的進一步拓展作著準備,在這里其實僅僅需要設置訂閱以及被訂閱的關系就行了。

好了,到了最關鍵的部分了,我們如何重寫Factory2,我們知道,activity和Factory2是一對一的對應關系,而且我們需要在該類中實現新建視圖邏輯,并且在里面加上我們所需的更改皮膚邏輯。

雖然說說是更改皮膚,其實無非是設置一些視圖的圖片,或者是顏色。

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]}/*** 觀察者模式接收到被觀察者更新的回調*/override fun update(p0: Observable?, p1: Any?) {SkinThemeUtils.updateStatusBarColor(activity)//更新皮膚中的信息skinAttribute.applySkin()}override fun onCreateView(p0: String, p1: Context, p2: AttributeSet): Nothing? = null }

可以操作的視圖分為兩種,一種是官方的一些視圖,對于這些視圖,我們的可控性其實非常高,只需要對于特定的一些屬性進行控制就行了。但是一些我們自定義的視圖,如果也按照這樣的邏輯進行篩選的話,可能會使邏輯過于復雜和冗余,所以這些視圖我們就特事特辦,讓他們繼承SkinViewSupport接口加以區分即可。

class SkinAttribute {companion object {//設置我們所感興趣的屬性val mAttributes = arrayListOf("background","src","textColor","drawableLeft","drawableTop","drawableRight","drawableBottom")}private val mSkinViews = arrayListOf<SkinView>()/*** 我們在factory2的onCreateView方法中添加了對于所創建的每一個視圖* @see com.ciruy.onion_plugin.SkinLayoutInflaterFactory.onCreateView(android.view.View, java.lang.String, android.content.Context, android.util.AttributeSet)* 可以操作的視圖分為兩種,一種是官方的一些視圖,對于這些視圖,我們的可控性其實非常高,只需要對于特定的一些屬性進行控制就行了* 但是一些我們自定義的視圖,如果也按照這樣的邏輯進行篩選的話,可能會使邏輯過于復雜和冗余,所以這些視圖我們就特事特辦,* 讓他們繼承SkinViewSupport接口加以區分即可*/fun look(view: View, attrs: AttributeSet) {val mSkinPairs = arrayListOf<SkinPair>()Array<Pair<String, String>>(attrs.attributeCount) {Pair(attrs.getAttributeName(it), attrs.getAttributeValue(it))}.filter {//如果屬性是我們所感興趣的屬性,并且并非通過硬編碼指定,執行后續操作mAttributes.contains(it.first) && !it.second.startsWith("#")}.map {//pair的首位是屬性的名稱,次位是SkinPair(it.first, when {//可能是直接調用系統直接提供的顏色或者是圖片,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)

總結

以上是生活随笔為你收集整理的Android插件化换肤的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。