Android(安卓)Android-skin-support 换肤方案 原理讲解
16lz
2021-01-24
文章目录
- 前言
- 思考一下
- 开源库中 找到答案
- 结束语
前言
请先查看这两篇文章
- LayoutInflater.Factory
- Android xml解析到View的过程
- Android 无需自定义View 解放 shape 解决方案 原理讲解
思考一下
上面已经说明了,我们自定义Factory2 就可以达到 无需shape的解决方案
那么同理,换肤我们怎么做呢?
先整理一下思路
- 自定义两个 color 的值 分别是
#ff000000 #ffffffff // 也就是main_color 白天的时候为 黑色 // 夜间的时候 为 白色
- 在自定义的 Factory2 (如WidSkinFactory2)中 创建自己的View 比如模仿 AppCompatActivity中 createView真正操作的类 AppCompatViewInflater.java
public class AppCompatViewInflater { final View createView(...){ switch (name) { case "TextView": view = createTextView(context, attrs); verifyNotNull(view, name); break; default: break; } } @NonNull protected AppCompatTextView createTextView(Context context, AttributeSet attrs) { return new AppCompatTextView(context, attrs); }}
- 既然 View 创建成我们自己的 比如 SkinCompatTextView , 那么我们不就可以轻松实现 属性获取,比如字体颜色 然后我们根据不同皮肤,加载不同颜色
- 这样不就可以换肤了
问题:
那么如果我们想动态换肤,怎么办?
耶? View都是我们自己创建的了,那么我们在 自定义Factory2 (如WidSkinFactory2)中,记录下这些 View,然后定义某个方法,刷新新的皮肤不就好了?
开源库中 找到答案
https://github.com/ximsfei/Android-skin-support
为了验证我们的想法更或者说是看看别人是否有更好的解决方案
我们来 提取关键几个类进行讲解
查看Application
这里的源码 其实就是注册一个 Application.ActivityLifecycleCallbacks
在 Lifecycle onActivityCreated() 方法中 进行 installLayoutFactory
也就是将系统的 Factory2 替换为 自己的 SkinCompatDelegate <继承于 LayoutInflaterFactory>
public class App extends Application { @Override public void onCreate() { super.onCreate(); // 框架换肤日志打印 Slog.DEBUG = true; SkinCompatManager.withoutActivity(this) .addInflater(new SkinAppCompatViewInflater()) // 基础控件换肤 ... .loadSkin(); AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); }}
SkinCompatDelegate.java
public class SkinCompatDelegate implements LayoutInflaterFactory { private final Context mContext; private SkinCompatViewInflater mSkinCompatViewInflater; private List> mSkinHelpers = new ArrayList<>(); private SkinCompatDelegate(Context context) { mContext = context; } @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { View view = createView(parent, name, context, attrs); if (view == null) { return null; } // 缓存 View 这里跟我们 上面思考的问题一样 // 记录View 用于后期动态换肤 if (view instanceof SkinCompatSupportable) { mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view)); } return view; } public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { if (mSkinCompatViewInflater == null) { mSkinCompatViewInflater = new SkinCompatViewInflater(); }... mSkinCompatViewInflater.createView(parent, name, context, attrs); }//换肤 public void applySkin() { if (mSkinHelpers != null && !mSkinHelpers.isEmpty()) { for (WeakReference ref : mSkinHelpers) { if (ref != null && ref.get() != null) { ((SkinCompatSupportable) ref.get()).applySkin(); } } } }}
SkinCompatViewInflater.java
public class SkinCompatViewInflater { private View createViewFromHackInflater(Context context, String name, AttributeSet attrs) { View view = null; // 根据 你在 application 中 add 的 Infater 循环判断,是否创建View成功 for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getHookInflaters()) { view = inflater.createView(context, name, attrs); if (view == null) { continue; } else { break; } } return view; }}
这里我们先分析个最简单的
SkinAppCompatViewInflater.java
public class SkinAppCompatViewInflater{ switch (name) { // 创建自己的View,也就是印证了 思考 <2> case "TextView": view = new SkinCompatTextView(context, attrs); }}
SkinCompatTextView.java
public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable { private SkinCompatTextHelper mTextHelper; private SkinCompatBackgroundHelper mBackgroundTintHelper; public SkinCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 换肤的帮助类,单一原则 mBackgroundTintHelper = new SkinCompatBackgroundHelper(this); mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr); mTextHelper = SkinCompatTextHelper.create(this); mTextHelper.loadFromAttributes(attrs, defStyleAttr); }// 动态换肤 @Override public void applySkin() { if (mBackgroundTintHelper != null) { mBackgroundTintHelper.applySkin(); } if (mTextHelper != null) { mTextHelper.applySkin(); } }}
SkinCompatTextHelper.java
public class SkinCompatTextHelper {public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) { // ... final Context context = mView.getContext(); a = context.obtainStyledAttributes(attrs, R.styleable.SkinTextAppearance, defStyleAttr, 0);// 颜色 if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) { mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID); } a.recycle(); // 执行 applySkin(); } @Override public void applySkin() {... applyTextColorResource(); } private void applyTextColorResource() { mTextColorResId = checkResourceId(mTextColorResId); // 是否有ID if (mTextColorResId != INVALID_ID) { // 通过 工具 传入 id 比如上面思考的 main_color // 如果是夜间 他就会 返回 main_color_night ColorStateList color = SkinCompatResources.getColorStateList(mView.getContext(), mTextColorResId); mView.setTextColor(color); } }}
SkinCompatResources 的源码就不讲解了,就是上面所说的
根据不同的模式,将原有ID_xxx模式,如 main_color_night
然后返回
当然这个类还有很多功能,我们这里只研究重点
结束语
- Android-skin-support 也是通过 自定义Factory的方式 实现
- 将系统传过来的 view 如 TextView 创建成 自己的 SkinCompatTextView
- 通过 xxx_Helper 进行资源操作,并对外提供 applySkin 换肤方法
- SkinCompatResources 根据不同皮肤 返回不同的颜色值 (只说思路,还有很多功能)
更多相关文章
- Android短信转发默认不需要转发号码修改方法
- Android实现ListView或GridView首行/尾行距离屏幕边缘距离
- Android(安卓)viewPager实现翻动
- Android高手进阶教程(十五)---Android中万能的BaseAdapter(Spinn
- [转载]Android实现更换皮肤功能
- Android高手进阶教程(十六)之---Android中万能的BaseAdapter(Spi
- Android(安卓)Studio LibraryModule中引用aar
- 菜鸟的安卓实习之路---- 如何实现android恢复出厂设置
- android沉浸式全屏显示