当了解了一些知识,应该用文字记录它,再抽个时间再看它,永远记住它

Android 换肤的理论知识和文章已经很多了,这里记录一下自己对这块的理解。本文效果如下:

工程:一键换肤的快乐

一、换肤的由来

首先,为什么要换肤呢?那肯定是一套UI不满足需求,无法面对多变的需求,从而需要有可以自由去更换UI 的手段,而这也是换肤想要达到的目的。

比如,一个imageview , 现在设置了一张图片,但是 618 来了, 我先更换成新的图片,怎么办?总不能让用户再更新一遍吧,虽然可以增量更新,但总不能每次都直接更新吧?

那我么一般怎么更新 imageview 的图片呢?

 ImageView imageView =  findViewById(R.id.image);imageView.setImageResource(R.mipmap.bg);

可以通过 setImageResource() 设置更新图片。

1.1 应用内换肤分析

那如果我下发了换肤命令,怎么更新呢?如果是应用内更新,那图片的名字肯定是不能一样的,不能R文件找不到;这个时候,我们可以新建一个 res_skin ,skin_bg 改个名字,比如 skin_bg。

然后在换肤命令来的时候,换成如下代码:

 imageView.setImageResource(R.mipmap.skin_bg);

那我换肤命令哪知道你有多少个 view 啊 ?怎么知道你要替换是 mipmap ,还是 color 啊?
别急,这个后面会讲。

1.2 插件换肤分析

上面是应用内换肤,如果是插件换肤呢。
插件换肤的话,就是把要替换的资源,比如上面的 bg 图片,放到一个apk 中,然后从这个apk 中取出这个资源,插件换肤不需要给资源名称,与原apk 保持一致即可。

怎么取呢,从上面 R.mipmap.bg 知道 ,所有得知道资源是从 mipmap 取,且名字叫做 bg 就可以取到这个 id 了。
幸运的是,Resource 有个方法:

    public int getIdentifier(String name, String defType, String defPackage) {return mResourcesImpl.getIdentifier(name, defType, defPackage);}

参数解释如下:

  • name:资源名称
  • defType : 资源类型,比如 mipmap,color,string…
  • defPackage : 目标包名

那这样的话,事实上,

 imageView.setImageResource(R.mipmap.bg);

也可以写成:

        int res = getResources().getIdentifier("bg","mipmap",getPackageName());if (res != 0){imageView.setImageResource(res);}

可以看到,确实显示出来了:

咦,那我只要去加载皮肤的资源包,再通过 resource 的 getIdentifier 不就可以拿到资源文件了吗,然后同通过 view 去设置就可以了。

那怎么去解析这个 皮肤资源包呢?
我们知道 Android 的资源管理,除了 Resource ,还有 AssetManager;其中 Resource 类可以通过 ID 来查找资源,而 AssetManager 则可以根据文件名来查找资源。

那这里就好办了,就使用 AssetManager ,然后它有个方法:

    /*** @deprecated Use {@link #setApkAssets(ApkAssets[], boolean)}* @hide*/@Deprecatedpublic int addAssetPath(String path) {return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);}

但这个是 hide 方法,且标注为 Deprecated,建议我们去使用setApkAssets,但 ApkAsset 又是 hide,难顶。

但笔者搜索了一下 setApkAssets 基本都是源码在使用,而主流的换肤,插件基本还是用 addAssetPath,且在 Android P 上试了一下,也没啥问题,所以这里也暂时用这个把。既然是 hide ,那肯定用反射了:

try {//拿到资源加载器AssetManager assetManager = AssetManager.class.newInstance();Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);addAssetPath.setAccessible(true);addAssetPath.invoke(assetManager, skinPath);} catch (Exception e) {LggUtils.e("SkinManager - loadSkinPath error: " + e.getMessage());e.printStackTrace();}

最后,还是要用 Resource去加载 id 的,所以,这里创建的 Resource,使用 assetmanager 参数的,

 Resources skinResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());

后面咱们就可以使用 skinResource 和 getIdentifier 去加载资源了。

Ok,两种原理都分析完了。上面遗留的问题就是:

  1. 如何获取需要换肤的 View
  2. 如何知道这个view的换肤属性,比如是 bitmap,还是 color等

下面一起解决这个问题。

二、View 的生成过程

从 activity 下手,一般我们都是 setContentView(R.layout.main_activity) 去设置我们的 xml,但有没有想过,为啥设置了这个方法之后,就能拿到 View 呢?
再抛出一个问题,比如你在 xml,写个 textview 和 button 如下:

    <TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="测试换肤"/><Buttonandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="换肤"/>

然后打开 Tool - Layout Insepctor 查看:

额,怎么我的 textview 和 button 变成了 AppCompatTextView 和 AppCompatButton 了?

带着这个疑惑,我们从 setContentView 跟踪下去:

    @Overridepublic void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);}

首先,如果你的 activity 继承 AppCompatActivity,那么它会通过 一个 Delegate 代理类去设置 setContentView,它是个抽象方法,它的具体实现类是AppCompatDelegateImpl,但为了更好的看到整个过程,我建议你把targetSdkVersion改成26,然后去看 AppCompatDelegateImplV9,原理都是一样的,这是更加清晰。
好了,题外话过,去到实现类的 setContentView,可以看到:

除了我们熟悉的 R.id.content,最重要的就是 LayoutInflater 的 inflate 方法了,进入看看:

可以看到,拿到了 resource 之后,通过 res.getLayout(resource) 去解析 xml 布局,最后继续执行 inflate 方法,继续看下去:

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {synchronized (mConstructorArgs) {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");final Context inflaterContext = mContext;final AttributeSet attrs = Xml.asAttributeSet(parser);Context lastContext = (Context) mConstructorArgs[0];mConstructorArgs[0] = inflaterContext;View result = root;...if (TAG_MERGE.equals(name)) {if (root == null || !attachToRoot) {throw new InflateException("<merge /> can be used only with a valid "+ "ViewGroup root and attachToRoot=true");}rInflate(parser, root, inflaterContext, attrs, false);} else {// Temp is the root view that was found in the xmlfinal View temp = createViewFromTag(root, name, inflaterContext, attrs);ViewGroup.LayoutParams params = null;.....return result;}}

这个方法会先去解析是否有自定义属性,然后可以从 xml 文件根部去解析;最重要的是里面有个方法 createViewFromTag,它是 view 生成的关键点,进入看看:

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {.... try {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);}if (view == null) {final Object lastContext = mConstructorArgs[0];mConstructorArgs[0] = context;try {if (-1 == name.indexOf('.')) {view = onCreateView(parent, name, attrs);} else {view = createView(name, null, attrs);}} finally {mConstructorArgs[0] = lastContext;}}return view;...

重点可以看到,View 的解析首先,会先判断 mFactory2 是否不为null,如果不是,则去通过 onCreateView 去创建这个 view,如果为 null,则判断 mFactory (其实如果你设置 mFactory ,到源码里面还是被替换成 mFactory2 的,具体自己跟踪),以此类推;

等等, 这个 mFactory2 哪来的?跟踪的时候没看到啊?
别急,当你继承 AppCompatActivity 的时候,我们进入看看

在 oncreate 方法的时候,有个 installViewFactory()方法,它的具体实现类是 AppCompatDelegateImpl ,可以看到:


恩恩,这个就好说了。

接着如果都找不到这个 view,则会通过 createView 这个方法去重新解析 View。去到 mFactory2 中的 onCreateView 方法,你是一个接口,具体实现类是 AppCompatDelegateImpl 或 AppCompatDelegateImplV9 (targetSdkVersion 26),看看里面的方法:

里面会把它再交给 mAppCompatViewInflater.createView(),然后可以看到:

    public final View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs, boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {final Context originalContext = context;...View view = null;// We need to 'inject' our tint aware Views in place of the standard framework versionsswitch (name) {case "TextView":view = new  (context, attrs);break;case "ImageView":view = new AppCompatImageView(context, attrs);break;case "Button":view = new AppCompatButton(context, attrs);break;case "EditText":view = new AppCompatEditText(context, attrs);break;case "Spinner":view = new AppCompatSpinner(context, attrs);break;case "ImageButton":view = new AppCompatImageButton(context, attrs);break;case "CheckBox":view = new AppCompatCheckBox(context, attrs);break;case "RadioButton":view = new AppCompatRadioButton(context, attrs);break;case "CheckedTextView":view = new AppCompatCheckedTextView(context, attrs);break;case "AutoCompleteTextView":view = new AppCompatAutoCompleteTextView(context, attrs);break;case "MultiAutoCompleteTextView":view = new AppCompatMultiAutoCompleteTextView(context, attrs);break;case "RatingBar":view = new AppCompatRatingBar(context, attrs);break;case "SeekBar":view = new AppCompatSeekBar(context, attrs);break;}if (view == null && originalContext != context) {// If the original context does not equal our themed context, then we need to manually// inflate it using the name so that android:theme takes effect.view = createViewFromTag(context, name, attrs);}if (view != null) {// If we have created a view, check its android:onClickcheckOnClickListener(view, attrs);}return view;}

真香最终打败了,原来如果 activity 继承 AppCompatActivity,则在内部,会把 textView 替换成 AppCompatTextView,这也是我们在 xml 中写 TextView,在 layout inspector 却显示 AppCompatTextView 的问题了。

当然,不是每个 view 都替换,如果找不到这个 view,则通过 createViewFromTag(context, name, attrs); 去解析:

可以发现,还是用了 createView 去解析,createView方法时通过 类加载去加载的,这里不深入了解了。

2.1 简单替换 View

从上面知道了,View 的生成在 mFactory2 中的 onCreateView 中,那么,这里,我们做个小实验,比如检测到 textview ,把它改成 button 试试,由于 AppCompatActivity 在 onCreate 之前就设置了 mFactory2,所以,我们自己的 factory 要放到 super.oncreate() 之前,如下:

    @Overrideprotected void onCreate(Bundle savedInstanceState) {LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {if (name.equals("TextView") ){Button button = new Button(context);button.setText("我被替换了");return button;}return null;}@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {//这个方法时 mFactory,因为 mFactory2 继承 mFactory ,所以可以不用管return null;}});super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);}

好了,看一下效果,换肤前:

换肤后:

可以看到,TextView 确实被替换了,不过我们看一下 layout insepctor:

咦,我的 Button 没有替换成 AppCompatButton了,为啥呢?
因为我们自己设置了 factory ,且在 onCreateView 回调的时候,直接返回 button了:

都没经过 系统的替换,那这里肯定没变了。那我想享受 AppCompat 带来的额外属性怎么办?

简单,我们自己不去创建 View,交还给系统去创建,把 name 改成 button 就可以了,如下:

public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {if (name.equals("TextView") ){name = "Button";}View view  = getDelegate().createView(parent,name,context,attrs);return view;
}

再看看 layout inspector:

三、实际应用

通过上面分析,你应该知道 factory 的作用,常见的实际应用有以下:

3.1 全局替换字体

有时候需要一键该字体,那我们检测到当前view 为 textview,全局替换即可,简单代码如下:

        final Typeface typeface = Typeface.createFromAsset(getAssets(),"yahei.ttf");LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {View view  = getDelegate().createView(parent,name,context,attrs);if (view instanceof TextView){TextView textView = (TextView) view;textView.setTypeface(typeface);}return view;}@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {//这个方法时 mFactory,因为 mFactory2 继承 mFactory ,所以可以不用管return null;}});

3.2 换肤

这个网上都很成熟的方法了,但自己搞一个不香吗;可以应用上面的知识尝试一下;

很多网上的换肤框架都需要继承 baseActivity,baseFragment 的,又或者说什么需要传递 context 的,比如 skinManager.with(this)。

额,其实这里有小技巧,其实我们在自己的库里,编写一个 contentprovider,从 onCreate 拿到 context,检测到这个 context 是application,就可以通过 application 去拿到所有的 activity 了。比如:

然后在 onActivityCreated 的时候,添加我们的皮肤注入即可,如下:


感兴趣可以看看这个:https://github.com/LillteZheng/ZSkinPlugin
效果如下:

3.3无需编写shape、selector,直接在xml设置值

前段时间火到爆的,原理也是用到 factory,上面的 contentprovider 小技巧也是参考这个的哦;

地址: https://juejin.im/post/5b9682ebe51d450e543e3495

这样,这篇文章就写完了。

参考:https://mp.weixin.qq.com/s/1ua0geFnrbQbyHi8KG2VJQ

Android 换肤原理分析相关推荐

  1. Android换肤原理

    qq 网易云音乐的换肤功能很炫酷,这里总结下换肤原理. 换肤分为两种模式,静态换肤 动态换肤.静态换肤就是把不同皮肤的资源打包到apk中,使用时切换, 这种换肤的弊端就不再多说了(种类固定,apk大) ...

  2. Android-skin-support 一款用心去做的Android 换肤框架

    介绍 Github地址: https://github.com/ximsfei/Android-skin-support Android-skin-support: 一款用心去做的Android 换肤 ...

  3. Android 插件换肤原理解析

    转至:http://blog.csdn.net/jiangwei0910410003/article/details/47679843 一.前言 今天又到周末了,感觉时间过的很快呀.又要写blog了. ...

  4. Android 换肤之旅——主题切换

    随着手机应用的成熟发展,市面上的应用已不在以简单的实现功能为目标了,它们反而会更加注重用户体验.我们常说的换肤(主题)功能--针对用户的喜好来提供一个可选的主题也是提高用户体验的方式之一.换肤功能不仅 ...

  5. Android 换肤方案详解(一)

    引言 在我们的开发中,也许有些项目会有换肤的需求,这个时候会比较头疼怎么做才能做到一键换肤呢?大家肯定是希望只要一行代码就能调用最好.下面我们先分析一下换肤的本质是什么? 原理 换肤,其本质无非就是更 ...

  6. Android-skin-support 换肤原理全面解析

    一.背景 公司业务上需要用到换肤.为了不重复造轮子,并且快速实现需求,并且求稳,,于是到Github上找了一个star数比较多的换肤框架-Android-skin-support(一款用心去做的And ...

  7. Android换肤总结

    文章目录 换肤方案 Theme换肤 Resouce换肤 2.拿到皮肤包Resource对象 3.标记需要换肤的View 4.缓存需要换肤的View 5.切换时即时刷新页面 6.制作皮肤包 UiMode ...

  8. android换肤动画,Android动态换肤框架-实现换肤

    1.换肤流程 1 2.采集流程 2 3.Android资源查找流程 3 4.采集需要换肤的控件 换肤我们需要换所有可能需要换的页面控件,所以我们不可能在每个页面重新findviewById,这时就需要 ...

  9. Android-skin-support 换肤原理全面解析 1

    文章目录 一.背景 二.demo 三.AppCompatActivity实现 四.Android创建View全过程解析 五.换肤原理详细解析 1.上文预备知识与换肤的关系 2.源码一,创建控件全过程 ...

最新文章

  1. Redis中的发布与订阅的概念与以命令行的方式实现发布订阅举例
  2. Winfrom中设置ZedGraph显示多个标题(一个标题换行显示)效果
  3. 如何查询oracle死锁,Oracle死锁查看和解决办法汇总
  4. JavaScript --- 表单focus,blur,change事件的实现
  5. 举例说明Java中代码块的执行顺序
  6. zabbix历史数据mysql_处理Zabbixl历史数据库解决办法三---使用MySQL中间件为Zabbix数据库扩容...
  7. 广和通再推5G利器,发布高性价比5G模组FM650
  8. 本部裁员、分部招人,科技公司的岗位都奔向了外地?
  9. Windows查询端口的进程
  10. 01、ZigBee 开发教程之进阶篇—BasicRF无线点对点传输协议
  11. Rect.OverLaps() 改进
  12. 【IT运维】国内优秀的IT运维企业有哪些?
  13. 简要介绍DES、RSA MD5 sha1 四种加密算法的优缺点
  14. shell脚本中实现远程和其他用户的子shell执行
  15. linux 添加动态链接库的方法
  16. 真相:为什么投简历总是没回音?
  17. 【OpenAirInterface知识-4】OAI端到端部署之UE部署
  18. 在线磁盘扩容 500G =》 2T 实战教程
  19. 【Qt象棋游戏】07_人机博弈算法开端
  20. 百度音乐 android,Android 百度音乐 - CNMO

热门文章

  1. 用计算机模拟进化论,意识并不神秘,电脑模拟的大脑居然产生意识
  2. python电影评论的情感分析流浪地球_爬虫实例 | Python爬取《流浪地球》豆瓣影评与数据分析(下)...
  3. 【Linux】设备树详解dts
  4. 从零搭建react + webpack项目
  5. 学习记录(一)制作python版本的CIFAR10数据集
  6. 计算方法Gauss-Jordan消去法求线性方程组的解
  7. 语言迁移提出20c50,语言迁移的研究综述.doc
  8. “终极PK”终告落幕,DEMO GOD花落谁家?
  9. mongo执行Bulk Update
  10. STM32 堆栈溢出检测