前言
前阵子看了bilibili上的一些技术相关影片,码牛学院的公开课程
Android动态加载技术的高级进阶,手写实现网易云主题换肤框架
影片中的讲者用Java初步实作了一个修改主题的框架
学习的过程动作做是个满重要的阶段
因此我把影片中的这个框架跟着教学手刻一次
刻完套用在我自己的测试专案上时发现还有些问题
接着就依照遇到的问题进行进一步的修改
在教学影片中看得到的逻辑规则,本篇文章就不赘述,我会直接说明我基于教学的框架增加或修改的部分
不想浪费时间看文章可以直接看我的Repository ChangeThemeSample
问题集
- 第一个遇到的问题是,依照我过去的开发习惯,专案经常会大量的使用到style来设定相同规格的物件
例如这样统一设定TextView的风格
<style name="style_button_text"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> <item name="android:textColor">@color/white</item> <item name="android:layout_gravity">center</item> <item name="android:gravity">center</item> <item name="android:background">@drawable/selector_btn_circle_default</item> <item name="android:paddingBottom">10dp</item></style> <TextView style="@style/style_button_text" />
在使用style的情况下,透过AttributeSet getAttributeName只会取到"style"
而style_button_text里面有设定的textColor, background是取不到的
这样在后续设定主题时,使用style的写法就会失效
因此需要针对style的况状更深入的处理
首先我定义了,方便foreach确认属性是否存在
private static final int[] NATIVE_ATTRIBUTE_ID = { android.R.attr.textColor, android.R.attr.background, android.R.attr.src};
逐一确认style中是否能够取到我们对应的属性
TypedArray typedArray = view.getContext().obtainStyledAttributes(attrs.getStyleAttribute(), NATIVE_ATTRIBUTE_ID);if (typedArray.length() > 0){ for(int ti = 0; ti < typedArray.length(); ti++){ try{ if(typedArray.hasValue(ti)){ int resId = typedArray.getResourceId(ti, -1); if(resId != -1) { addSkinItem(view, NATIVE_ATTRIBUTE_NAME[ti], resId, skinItems); } } } catch (Exception e) { e.printStackTrace(); } }}typedArray.recycle();
这边有遇到一个问题还没深入去找原因,NATIVE_ATTRIBUTE_ID中将textColor及background顺序调换后,textColor会失效取不到resourceId,之后有空必须要查查
- 第二个问题是,不支援专案正在使用的物件或属性
解决方案当然就是要把物件及相应的属性也加入到判断的清单
private static final String[] THIRDPARTY_ATTRIBUTE_NAME = { "tabIndicator", "tabIndicatorColor"};private static final String[] THIRDPARTY_VIEW = { "com.google.android.material.tabs.TabLayout"};
利用反射的方式将数值置换
Method setSelectedTabIndicator = view.getClass().getDeclaredMethod("setSelectedTabIndicator", Drawable.class); setSelectedTabIndicator.invoke(view, SkinManager.getInstance().getDrawable(skinItem.getResId()));
- 第三个问题则是由第二个问题衍伸
Android的物件、属性千百种,实在不太可能每一个都加入我们的判断规则中,就算都整理进来多少也会有效能上的问题,例如每个onCreateView都要foreach跑一次包含2000项目的array大概受不了
因此需要增加一个可以让人扩充判断及设定的功能
这边以ProgressBar及progressDrawable属性作为範例
首先需要生成一个callback(CustomViewAttributeApplyListener),这个callback会将当下要置换主题的view、属性及目前取到的资源回传,开发人员收到此回传实再进行相对应的设定
这边比较需要注意点的点是,进行设定要取得资源时必须透过SkinManager中的getDrawable、getColor相关方法取得,如果直接使用当下的conetext取回资源则会取到原始APK的资源,而非主题包
CustomViewAttributeApplyListener listener = (view, fieldName, resId) -> { String viewName = view.getClass().getSimpleName(); switch (viewName){ case "ProgressBar": ((ProgressBar)view.findViewById(R.id.progressBar)).setProgressDrawable(SkinManager.getInstance().getDrawable(resId)); break; }};SkinManager.getInstance().addCustomView(new SkinCustomView(ProgressBar.class.getSimpleName(), ProgressBar.class.getName(), new String[]{"progressDrawable"}, listener));
- 最后一个问题
大概是每个专案都一定会遇到的状况,某些UI上效果需要依照API回传的状态呈现
例如状态1文字使用颜色A、背景使用AA;状态2文字使用颜色B、背景使用BB
这个状况再设定时就需要参考问题三
同样是使用SkinManager中的getDrawable、getColor相关方法取得资源
总结
个人认为看完公开课程还算满有收穫的,也算是有个现成的可以学习,也讲解得满详细的,后面进阶课销售相关的话真的很多。不过也可以透过他讲述的一些状况了解内地的行情可能也不是坏事
这个套件肯定还有许多需要优化的地方,以及Bug需要修改,有任何建议欢迎提出或加入一起维护