文档章节

Android 颜色色值与 alpha 分离解决方案

字数 2808
阅读 29
收藏 0

一、背景

目前 Android 并不支持 xml 文件中颜色与透明度分开定义,如果想用带透明度的颜色值,只能在 colors.xml 文件中定义一个新色值。比如,有一个颜色名字叫 N900,定义如下:

<color name="N900">#1F2329</color>

当我需要一个 50% 透明度 N900 的颜色时,只能自己定义再一个色值:

<color name="N900_alpha_50">#7F1F2329</color>

于是,colors.xml 内就出现很多不规则颜色,就像下面这样子:

并且还会继续增加这些不规则颜色。当下次换颜色时,这些带透明度的颜色每一个都需要更换,维护起来十分麻烦。并且,这些颜色目前所在的module已经打成aar,每次如果需要增加新的颜色,都需要重新打包aar上传,十分影响开发效率。

为了解决上述问题,开发了ResKitPlugin 插件,在编译时期动态替换颜色,支持颜色与透明度分开定义。

二、技术原理

基本思路是在aapt最终打包前,替换资源编译后生成的文件,使 aapt 最后打包时使用的资源二进制文件内的相应的颜色值已经带上了透明度。先上一张图:

下面详细介绍:

(一).相关背景知识介绍

  1. 目前我们的资源编译使用的都是aapt2, aapt2 编译资源分两步:
  • 编译:将资源文件编译为.flat 文件
  • 链接:将.flat 文件链接为最终的二进制资源文件.ap_
  1. gradle 在编译apk 时,是执行一系列Task,并且有些Task 是有严格先后顺序的。
  2. 我们需要关心的是其中两个和资源编译相关的Task :
  • mergeDebugResources : 这个Task 是负责收集所有的资源文件并使用aapt2编译成.flat文件,放在build/intermediates/res/merged/{*flavor*}/{buildType}目录下。
  • processDebugResources:这个Task是负责 使用aapt2 将.flat文件 链接为最终的二进制资源文件
  • mergeDebugResources 与 processDebugResource有严格的先后顺序,先执行mergeDebugResources,后执行 processDebugResource
  1. gradle 的API支持 “改变Task的执行顺序" 的操作
  2. MergeResources 类的 computeResourceSetList 方法可以获取编译要用的全部 res 路径
  3. mergeDebugResources 执行后,所有res/values目录下的内容,都会合并到一个文件内,放在build/intermediates/incremental/merge${variantName}Resources/merged.dir/values/values.xml内,这里面包括了定义的颜色资源。

(二).实现步骤

  1. 首先,定义一个自己的Task,叫做handleAlphaColorTask,负责修改.flat文件
  2. 通过gradle 的 API , 将handleAlphaColorTask 插入 mergeDebugResources 与 processDebugResources之间,这一步执行后,gradle 的编译Task 的 调用顺序如下:

  1. 在handleAlphaColorTask内处理mergeDebugResources生成的文件,使其内部的颜色属性带上了透明度,例子如下:

最开始,drawable_a.xml.flat 文件是由(代码-1)编译生成:

   <solid android:color="@color/N900" android:alpha="0.5" />

通过我们的处理,drawable_a.xml.flat 文件 变成了由(代码-2)编译生成:

  <solid android:color="@color/reskit_tmp_color_N900_alpha_0_5" />

对于硬编码的颜色,会直接进行如下转换:

  <solid android:color="#1F2329" android:alpha="0.5" />
              |
             \|/
  <solid android:color="#7F1F2329"/>

通过这样的处理,我们的颜色在运行时就拥有了透明度。下面介绍具体处理的步骤。

(三). 颜色转换的具体实现方式

通过computeResourceSetList去获取到所有参与编译的资源文件,然后修改源码,编译生成新的.flat文件,并替换原来的.flat文件。分为以下几步:

  1. 通过反射调用MergeResources 的 computeResourceSetList 方法,获取参与编译的全部 res 文件夹路径,包括aar内的。
  2. build/intermediates/incremental/merge${variantName}Resources/merged.dir/values/values.xml内挑出所有的颜色定义并生成colors.xml,为后续根据id找颜色值提供基础。
  3. 遍历所有 res 文件夹下的xml 文件。
  4. 通过xml 解析,识别 颜色属性和与之配对的透明度属性,并通过计算生成最终的颜色值。如果颜色属性是引用属性,则去colors.xml 根据引用id 找到对应的色值,然后计算出最终颜色。计算出最终颜色后,需要替换颜色属性,进行替换时有以下两个策略
    • 原颜色属性是硬编码颜色时,如 android:color ="#1F2329",则直接修改值即可。
    • 原颜色属性是引用颜色时,如android:color="@color/lkui_N900",会生成一个新key,然后将其替换为新key,并把这个新key与颜色的对应关系存在一个Map里,待新key 全部生成后,统一将新key 与颜色的对应关系写入build/intermediates/incremental/merge${variantName}Resources/merged.dir/values/values.xml文件,参与后续编译。
  5. 遍历期间,将需要修改的文件,则保存下来,放到一个Map里,这么做的目的是当出现同名资源时,提供筛选资源的数据。Map的定义如下:
Map<String, Map<String, String>>

资源文件的父文的名字 + “/” + 资源文件的名字  :  [  原始文件全路径  : 处理了alpha 后的新文件的全路径  ]

举例:
drawable/aab.xml : [/Users/guoxiao/ResPluginDemo/app/src/main/res/drawable/aab.xml  :  /Users/guoxiao/ResPluginDemo/app/build/coloralpha/res/drawable/aab.xml]
  1. 遍历完成后,我们就得到了一个Map和在指定目录下合并了alpha属性的资源 源码文件,接下来,需要处理重名资源
  2. 我们需要知道在intermediates/res/merged/{*flavor*}/{buildType}目录下,对于重名文件来说,系统究竟使用了哪个文件去参与编译的。这里使用的方法是:
    • 在第5步获得的map里,可以知道有几个重名文件,全路径是什么,对应的新的修改后的文件是什么。
    • 编译每一个重名的原文件,生成.flat文件,然后和intermediates/res/merged/{flavor}/{buildType}目录下的同名文件做md5比较,比较结果相同的,说明找到了系统编译使用的文件
    • 找到了系统编译是用的文件,我们就知道了最后我们应该编译哪个新的修改了属性的文件去替换原.flat文件
  3. 重名文件处理完成后,就可以编译新的修改后的文件,产生新的.flat 文件
  4. 用新的.flat 文件 替换老的.flat 文件。
  5. 如果本次编译没有修改资源文件,即intermediates/res/merged/{flavor}/{buildType}目录下的文件的md5和上次一致,则直接使用上次的缓存的.flat文件进行替换

三.对编译性能的影响

我们缓存了上次颜色处理得到的.flat文件,对于本次颜色处理:

  1. 未命中缓存时:
    • 在CI 平台上测试:替换了78个.flat文件,用时13991ms,其中,替换资源文件用时10135ms(未指定文件夹过滤,此时是最坏情况,全量遍历), 编译用时352ms。一个文件的编译时间4.5ms左右。
    • 本地测试:替换了78个.flat文件,指定文件夹过滤 ,耗时6192ms,替换颜色3995m,编译255ms;未指定文件夹过滤,耗时9179ms左右, 替换颜色 7392 ms, 编译 184ms。
  2. 命中缓存时,耗时500ms左右

四.探索的过程

目前的实现并不是最初的方案,测试时,替换7个文件,耗时从30s ,到20s, 最终优化到现在的5s左右。下面介绍这期间经历的几个方案:

  1. 最初方案是在processDebugResources 后插入颜色处理Task:
    1. 获取到系统连接后的资源文件包.ap_
    2. 通过ApkTool反编译.ap_文件,得到源码
    3. 修改源码
    4. 重新编译新修改的源码,获得新的.flat文件
    5. 将新的.flat文件与系统编译产生的.flat文件一起参与链接,生成最终的二进制文件
    6. 用新获得的由新的源文件编译链接而产生的二进制文件提替换.ap_文件内的文件
    7. 处理完成

这种方式,一次链接耗时7s左右,而且为了链接,还需要做一些压缩与解压的文件操作,压缩全部.flat文件需要7秒多,反编译.ap_又需要7秒多,最终一次颜色处理下来,耗时30s左右。

  1. 第二种方案,在mergeDebugResources 和 processDebugResources之间插入颜色处理Task:
    1. 获取mergeDebugResources 后生成的.flat文件,链接这些.flat文件,生成tmp.ap_包
    2. 反编译tmp.ap_,得到源码
    3. 修改源码
    4. 重新编译修改了源码的文件,得到.flat文件
    5. 替换mergeDebugResources 生成的同名.flat文件
    6. 处理完成

这种方式,可以去除对系统生成的.ap_文件的修改,耗时20s左右。

​ 前两种方案耗时,主要是进行链接和反编译,从而得到源码。于是思考,有没有可能不通过链接和反编译的方式来得到源码,最终有个方案3。

  1. 第三种方案,依然是 在mergeDebugResources 和 processDebugResources之间插入颜色处理Task,区别于第二种方案,是通过反射MergeResource的 computeResourceSetList 得到所有参与编译的资源文件(/res):
    1. 通过反射调用computeResourceSetList 获得所有的资源目录
    2. 遍历资源目录,如果需要处理颜色,则拷贝一份新文件,然后处理并保存到指定目录build/coloralpha/res
    3. 处理完成后,编译build/coloralpha/res生成新的.flat文件
    4. 用新的.flat文件替换系统编译生成的同名.flat文件
    5. 处理完成

最终耗时5s 左右。

处理完成后,系统的processDebugResources就会使用我们处理过的.flat文件。

五.特别注意的坑

mergeResource Task 若使用了 gradle 的构建缓存(运行该Task 会输出 FROM_CACHE) ,会缺失这个Task的中间产物,即merged.dir文件夹为空。

针对这种情况,我们每次在MergeResource执行前判断是否有merged.dir,若没有,不让它走 FROM_CACHE。

具体做法是:临时生成一个资源文件,导致缓存失效,这样就会触发mergeResources走一遍,然后 在mergeResource之后删除我们临时生成的资源文件。

六.一些想法

由于我们可以拿到参与编译的所有资源文件,也可以修改替换系统编译产生的文件。这两个能力,提供了巨大的想象空间,如:

  1. 可以做全局资源查重,包括aar内的资源
  2. 可以做资源压缩,如压缩图片
  3. 可以支持更多的类似color,alpha这样的组合属性的自定义
  4. 可以根据编译环境修改string.xml 内容
  5. 编译过程中自动收集生成皮肤包

最后

如果你看到了这里,觉得文章写得不错就给个赞呗!欢迎大家评论讨论!如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足,定期免费分享技术干货。感兴趣的小伙伴可以点一下关注哦。谢谢!

© 著作权归作者所有

粉丝 0
博文 17
码字总数 32343
作品 0
长沙
私信 提问
Android通知栏介绍与适配总结(上篇)

此文已由作者黎星授权网易云社区发布。 欢迎访问网易云社区,了解更多网易技术产品运营经验。 由于历史原因,Android在发布之初对通知栏Notification的设计相当简单,而如今面对各式各样的通...

网易云
2018/11/29
0
0
android 使用透明色

在ios里面用惯了透明色,view.alpha,感觉很方便。想在android中也设置一下。看看效果: 上面的文字,是TextView,背景是黑色的,但是使用了透明色。 这部分东西,不需要动一点java代码,在布...

华宰
2011/09/07
2.5K
0
自定义Android的ListView布局和各Item的背景色

Android中的ListView是用得非常频繁的一种组件,同时ListView也是一种很强大的组件,你可以为每一行自定义布局,也可以修改各行的 背景色。自定义布局比较容易,自己实现一个layout的布局文件...

LiSteven
2013/03/28
5K
0
Android 使用shape来优化界面效果

看下效果图 shape即形状的意思。其包含的元素有 <corners /> <gradient /> <padding /> <size /> <solid /> <stroke /> 元素具体描述信息如下: 1、 solid 描述:内部填充 属性: android:co......

hhs
2012/08/29
1K
0
android沉浸式状态栏封装—教你玩出新花样

项目中我们有时候都要用的透明状态栏(这里也成沉浸式状态栏),今天介绍一个gyf-dev写的一个封装状态栏开源框架 效果图如下: 正文 从Android4.4开始,才可以实现状态栏着色,并且从5.0开始...

终端研发部
2018/12/19
210
0

没有更多内容

加载失败,请刷新页面

加载更多

解答二进制求和

思路:创建一个新的字符串,用于记录原两个字符串每位相加的结果。 1、因为是从左到右计算,所以要把字符串先进行反转,用reverse()方法。 2、字符串对齐,采用补零的方法。 3、计算的时候...

无名氏的程序员
11分钟前
1
0
JSONUtils

package com.demo.utils;import java.util.ArrayList;import java.util.HashMap;import java.util.Iterator;import java.util.List;import java.util.Map;import java.util.Tr......

任梁荣
11分钟前
4
1
在jest中配置typescript

测试是报错: Property 'assign' does not exist on type 'ObjectConstructor' NodeJS已经是最新版了,但道理不需要polyfill。 然后发现是typescript的lib没有"es2015.core",说明ts-jest没有......

linsk1998
12分钟前
2
0
Redis实现分布式文件夹锁

缘起 最近做一个项目,类似某度云盘,另外附加定制功能,本人负责云盘相关功能实现,这个项目跟云盘不同的是,以项目为分配权限的单位,同一个项目及子目录所有有权限的用户可以同时操作所有...

逸竹小站
21分钟前
2
0
Andorid SQLite数据库开发基础教程(2)

Andorid SQLite数据库开发基础教程(2) 数据库生成方式 数据库的生成有两种方式,一种是使用数据库管理工具生成的数据库,我们将此类数据库称为预设数据库,另一种是使用代码生成的数据库。...

大学霸
41分钟前
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部