android view设置按钮颜色_Android 主题换肤技术方案分析
寫在前面
Android TV 電視開發(fā),主題換膚,我感覺有兩種層級(jí)的方式,一種是 系統(tǒng)級(jí),另一種 是應(yīng)用級(jí),
我記得很早在 Linux 桌面開發(fā)的時(shí)候,我們之前的公司在GTK+上也實(shí)現(xiàn)了一套換膚UI框架,
包括我們看到的QQ,掘金,簡(jiǎn)書,等應(yīng)用,無(wú)縫切入主題,不需要重啟應(yīng)用。
為何需要主題切換,因?yàn)閁I庫(kù)改來(lái)改去也就那些東西,不過在變換一些樣式,陰影,圓角,文本大小,顏色,邊框,背景 等等而已。
那么其實(shí)我們可以通用主題的配置方式,統(tǒng)一配置更改樣式.
幾種方案探討
下面探討的方案,都是不需要重啟應(yīng)用,及時(shí)生效的方案。 主題切換的問題在于,新的出來(lái)的界面,還有已經(jīng)存在的界面。
QMUI 主題換膚方案
QMUI的方案,定義各種 theme style,通過遍歷已存在的View,然后設(shè)置相關(guān)屬性,并且設(shè)置 Theme.;包括后續(xù)新出的控件也是已切換的主題樣式.
public void changeSkin(Object ob, int skinRes, Resources resources) {View view = null;if (null != ob) {if (ob instanceof Activity) { // 普通的 Activity支持Activity activity = (Activity) ob;Drawable drawable = getAttrDrawable(activity, activity.getTheme(), R.attr.tvui_commonItem_detailColor);activity.getWindow().setBackgroundDrawable(drawable);// 獲取 content view.view = activity.findViewById(android.R.id.content);} else if (ob instanceof Dialog) { // 系統(tǒng)設(shè)置5.0 - Dialog 主題切換支持Window window = ((Dialog) ob).getWindow();if (window != null) {view = window.getDecorView();}} else if (ob instanceof View) { // 普通的 View 主題切換支持view = (View) ob;}mResources = resources != null ? resources : view.getResources();if (null != view) {// 切換主題樣式theme = mResources.newTheme();theme.applyStyle(skinRes, true);// 設(shè)置背景顏色ColorStateList bgColorStateList = getAttrColorStateList(mContext, theme, R.attr.tvui_main_layout_background_color);view.setBackgroundColor(bgColorStateList.getDefaultColor());// 切換主題樣式,遍歷所有子View...setThemeDatas(view, skinRes, theme);}}
}// 遍歷Views切換主題樣式
private void setThemeDatas(View view, int skinRes, Resources.Theme theme) {applyTheme(view, skinRes, theme);if (view instanceof ViewGroup) {ViewGroup viewGroup = (ViewGroup) view;for (int i = 0; i < viewGroup.getChildCount(); i++) {setThemeDatas(viewGroup.getChildAt(i), skinRes, theme);}}
}private void applyTheme(View view, int skinRes, Resources.Theme theme) {if (view instanceof Button) {((Button)view).setTextColor(getAttrColorStateList(mContext, theme, R.attr.tvui_text_color).getDefaultColor());}
}// 添加了測(cè)試的 背景顏色與文本顏色
<attr name="tvui_main_layout_background_color" format="color"/>
<attr name="tvui_text_color" format="color"/><style name="NewAppTheme" parent="xxxx"><item name="tvui_main_layout_background_color">#FF0000</item><item name="tvui_text_color">#0000FF</item>
</style>// 切換主題
SkinManager.getInstance(AnimHomeActivity.this).changeSkin(AnimHomeActivity.this, R.style.NewAppTheme);
切換主題效果如下
主題切換示例代碼
上面會(huì)存在一個(gè)問題,已經(jīng)切換了主題樣式,新出來(lái)的界面控件這么辦??????? 為了使后續(xù)啟動(dòng)的 新出來(lái)的界面控件也是切換后的主題,需要作出如下修改
public void changeSkin(Object ob, int skinRes, Resources resources) {View view = null;... ...view.setBackgroundColor(bgColorStateList.getDefaultColor());// 新增 隨意一個(gè)都可以,設(shè)置后,后續(xù)新出來(lái)的控件都是現(xiàn)在切換的主題樣式view.getContext().setTheme(skinRes);//view.getContext().getTheme().setTo(theme);
}// 主題 style
<attr name="TVUIRoundButtonStyle" format="reference"/>
<declare-styleable name="UIRoundButton"><attr name="tvui_text_color"/>
</declare-styleable>// 默認(rèn)主題使用
<style name="TVUITheme.UIRoundButton"><item name="tvui_text_color">#FFEB3B</item>
</style>// NewAppTheme 使用
<style name="TVUITheme.UIRoundButton.New"><item name="tvui_text_color">#2196F3</item>
</style><style name="NewAppTheme" parent="xxxxx"><item name="tvui_main_layout_background_color">#FF0000</item><item name="tvui_text_color">#0000FF</item><item name="TVUIRoundButtonStyle">@style/TVUITheme.UIRoundButton.New</item>
</style>// 測(cè)試UIButton代碼
public class TVUIButton extends Button {public TVUIButton(Context context) {this(context, null);}public TVUIButton(Context context, AttributeSet attrs) {this(context, attrs, R.attr.TVUIRoundButtonStyle);}public TVUIButton(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.UIRoundButton, defStyleAttr, 0);int textColor = ta.getColor(R.styleable.UIRoundButton_tvui_text_color, Color.BLACK);setTextColor(textColor);setTextSize(152);ta.recycle();}
}
新增控件的效果
新增的相關(guān)代碼
補(bǔ)充,如果感覺麻煩,也可以直接在布局里面使用 attr 的屬性,就不需要定義一堆的和button相關(guān)的Style. 但是前提是你需要定義好你自己的 相關(guān)值,比如陰影多少,圓角的角度,一級(jí)文本,二級(jí)文本屬性等等,這里可以稱為 通用屬性.
<attr name="tvui_text_color" format="color"/><style name="NewAppTheme" parent="xxxx"><item name="tvui_text_color">#0000FF</item>
</style>// 布局里面使用
android:textColor="?attr/tvui_text_color"
// 代碼中使用
ColorStateList textColor = SkinManager.getAttrColorStateList(getContext(), R.attr.tvui_text_color);「這套主題的優(yōu)/缺點(diǎn):」
- 簡(jiǎn)單方便,適用于 只有亮/暗的主題切換.
- 缺點(diǎn)也明顯,如果要新增主題,就要更改或者新增,很麻煩,會(huì)導(dǎo)致重新發(fā)布APK.
當(dāng)然,因?yàn)槟承┚窒扌?#xff0c;肯定使用場(chǎng)景不同. 如果你是那種需要下載以及加載各種 酷炫主題包的,這種方案肯定不適合你. 如果只是UI庫(kù)統(tǒng)一配置(新的UI界面,只需要再原來(lái)的基礎(chǔ)繼承,就可以產(chǎn)生一套新的),只有亮/暗主題切換,簡(jiǎn)單,方便.
「參考資料:」 Android如何在代碼中獲取attr屬性的值
Andrid-Skin-Loader 主題切換方案
git項(xiàng)目地址:
Andrid-Skin-Loader 流程步驟:
- 初始化加載APK主題包
- 通用 Factory 遍歷 View,保存相關(guān)View與屬性的信息
- 最后切換主題的時(shí)候,重新調(diào)用 SkinManager 來(lái)獲取相關(guān)主題包的資源屬性.
「加載APK主題包」
// 這里會(huì)有點(diǎn)卡,建議放在異步線程里面執(zhí)行相關(guān)操作
String skinPkgPath = "皮膚的apk路徑";
File file = new File(skinPkgPath);
if(file == null || !file.exists()){return null;
}PackageManager mPm = context.getPackageManager();
// getPackageArchiveInfo文章: https://blog.csdn.net/qq379454816/article/details/50402805?locationNum=10&fps=1
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;AssetManager assetManager = AssetManager.class.newInstance();
// 調(diào)用addAssetPath方法將皮膚包加載進(jìn)內(nèi)存
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);Resources superRes = context.getResources();
Resources skinResource = new Resources(assetManager, superRes.getDisplayMetrics(),superRes.getConfiguration()); // apk皮膚包的資源句柄skinPath = skinPkgPath;
isDefaultSkin = false;return skinResource;「Factory 遍歷 View,保存相關(guān)view信息」 因?yàn)樽鲋黝}切換的,已經(jīng)顯示出來(lái)的沒有辦法更新,所以需要保存相關(guān)信息,切換的時(shí)候,就更新這些view的信息,達(dá)到主題切換的效果
public class SkinInflaterFactory implements Factory {public View onCreateView(String name, Context context, AttributeSet attrs) {view = LayoutInflater.from(context).createView(name, "android.view.", attrs);// 創(chuàng)建 view.view = LayoutInflater.from(context).createView(name, null, attrs);// 這里可以判斷 view 的 name,是不是 原生的,你也可以替換成你的//return view;}// 解析view的 attrs 的屬性,保存view的相關(guān)信息private void parseSkinAttr(Context context, AttributeSet attrs, View view) {}
}// Activity或者其它地方如何使用??
getLayoutInflater().setFactory(new SkinInflaterFactory());但是如果要達(dá)到遍歷View的效果,也可以參考 changeSkin,不一定要保存相關(guān)信息的
「SkinManager 相關(guān)函數(shù)」
// 獲取顏色值 比如 <color name="tvui_bg_color">#ecf0f1</color>
public int getColor(int resId){int originColor = context.getResources().getColor(resId);// 如果APK資源 mResources為空 或 使用默認(rèn)的,就返回當(dāng)前APP默認(rèn)的資源if(mResources == null || isDefaultSkin) {return originColor;}// 獲取 ID 的字符串名稱 ,這里返回的是 tvui_bg_colorString resName = context.getResources().getResourceEntryName(resId);// getResourceTypeName(resId) 獲取 color, drawable 等類型,這里返回的就是 color// 通過APK資源的 mResources 獲取APK 相關(guān)的資源.(類型,名字相關(guān),值不一樣的)// 比如APK資源里面是這樣:<color name="tvui_bg_color">#FF0000</color>int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);int trueColor = 0;try{trueColor = mResources.getColor(trueResId);} catch(NotFoundException e) {e.printStackTrace();trueColor = originColor;}return trueColor;
}
// 獲取Drawble
getDrawable... ...
上面已經(jīng)提到當(dāng)前已經(jīng)顯示的,那么后面新出來(lái)的控件這么辦?這個(gè)就需要再代碼里面編寫相關(guān)代碼才行.
有人肯定會(huì)想,我將 Activity 的 getResources 變成 APK資源的 resources 不就完啦,哈哈哈,肯定不是這樣的.
public Resources getResources() {Resources resources = SkinManagerApk.getInstance().getResources();if (null != resources) {return resources;}return super.getResources();
}
// getResources().getColorStateList(R.color.news_item_text_color_selector);
不能以上面的方式獲取,因?yàn)?getColorStateList(... ...); 這個(gè)資源的ID是本APK的,不是主題資源APK里面的.那要這么辦? 只能使用 SkinManager getColor 類似的方式. 通用 getResourceEntryName 獲取到 news_item_text_color_selector 的名稱. 然后再 通用 mResources.getIdentifier("news_item_text_color_selector", "color", skinPackageName); 獲取資源APK的ID. 最后通用 mResources.getColorStateList(trueResId); 獲取相關(guān)資源. 具體代碼參考:
- Andrid-Skin-Loader 的 SkinManager
- 也可以參考我縮減的代碼 SkinMangerApk
DEMO代碼
「優(yōu)缺點(diǎn)探討:」
- 可以進(jìn)行主題包下載,多主題更新,非常方便
- 局限性就是 你再 XML布局寫的默認(rèn)的,下次顯示出來(lái)還是原來(lái)默認(rèn)的,就需要再代碼里面進(jìn)行 已經(jīng)切換主題的 資源獲取,用代碼填充上去,侵入式的.
- 每次都要重新加載APK資源包
「參考資料:」 Android換膚原理和Android-Skin-Loader框架解析
熱更新資源替換方案
我們知道,系統(tǒng)級(jí)的替換主題都是 替換的資源ID. 那我們思考一下,能不能有一種主題切換方式,我們加載的主題APK資源包,就能替換當(dāng)前APP默認(rèn)的資源,我們不用手寫代碼去填寫覆蓋?也能及時(shí)更新資源?
按照熱更新的例子,資源是可以替換的.
相關(guān)代碼: https://gitee.com/hailongqiu/OpenDemo/tree/master/app/src/main/java/com/open/demo/skin/instant
參考資料: https://www.cnblogs.com/yyangblog/p/6252490.html
AOP方案切換主題
經(jīng)過我查詢,原來(lái)可以替換 resource,做一個(gè)鉤子;那么我們獲取到APK資源的 resource,替換掉其它相關(guān)的 resource 應(yīng)該就可以達(dá)到這樣的效果。包括布局XML的,新出來(lái)的等等... ..
代理 View.OnClick... Window.Callback 相關(guān)函數(shù),鉤子...
View.AccessibilityDelegate
ASM,AST
Android AOP三劍客:APT, AspectJ 和 Javassist
- APT應(yīng)用:Dagger,butterKnife,組件化方案等等
- AspectJ:主要用于性能監(jiān)控,日志埋點(diǎn)等
- Javassist:熱更新(可以在編譯后,打包Dex之前干事情,可以突破一下限制)
參考資料
LayoutInflater.SetFactory()學(xué)習(xí)(2)
QMUI換膚方案
Android-Skin-Loader
Android系統(tǒng)資源訪問機(jī)制的探討,小米科技董紅光:Android系統(tǒng)如何實(shí)現(xiàn)換膚
Android 資源加載機(jī)制詳解
Android 換膚那些事兒, Resource包裝流 ?AssetManager替換流?
《Android插件化開發(fā)》,《深入探索安卓熱修復(fù)技術(shù)原理》,《Android 全埋點(diǎn)解決方案》
總結(jié)
以上是生活随笔為你收集整理的android view设置按钮颜色_Android 主题换肤技术方案分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python可以调用Gpu吗_加快Pyt
- 下一篇: excel公式编辑器_职场办公技巧—Wo