但是还有一些问题并没有解决,比如我们正在吭哧吭哧的写代码,测试突然来让你给他打个包,必须要暂停当前的工作,去对应分支执行打包命令,打包期间也只能等着,不能继续写代码。
这时,Jenkins 服务器就派上用场了,我们可以将打包命令在 Jenkins 服务器进行配置,让测试人员自己去选择想要的分支版本进行打包,同时提升双方的效率。
Jenkins 相关的知识网上有很多文章,请大家自行搜索了解,本文只讲解在 Jenkins 上配置以下几点内容:
由于将 360 加固替换为梆梆加固,而梆梆加固的命令行工具不支持自动签名和本地多渠道包生成,所以增加了重新签名和 VasDolly 多渠道打包的命令。
完整的 config.gradle
配置:
1 | ext { |
在jiagu.gradle
中实现加固签名和多渠道包逻辑:
1 | import org.apache.tools.ant.taskdefs.condition.Os |
我们只需要在命令行执行 ./gradlew releaseApp1
就会执行编译—>加固—>签名—>生成多渠道包
上传蒲公英的代码和前文一样,只需要改动一下 apk 路径,取生成的渠道包进行上传。
首先我们创建好任务,配置 task 命令参数:
再配置 Gradle 插件执行上面选择的命令,并配置 Project properties:
如果执行上传蒲公英的任务,我们希望直接显示蒲公英的二维码,增加如下Set build description
配置:
我们保存配置,执行任务:
执行成功后的效果:
全文没有做太多讲解,直接上代码上配置图片,简单粗暴,如果 Gradle 部分有疑惑的建议看看前面几篇文章。
对于 Jenkins 的配置本文只是一笔带过,相信大家简单了解一下相关的概念和原理就能快速上手,基本流程跑通以后可以举一反三实现更多功能。
最后上 demo 地址:https://github.com/imliujun/GradleTest/tree/Jenkins
我们的超管包是需要发给运营人员去使用的,防止泄露导致的安全风险,我们希望对超管包先进行加固然后再上传到蒲公英。
我们的应用在发布的时候一般都需要进行加固和生成多渠道包,大家通常的做法应该是下载加固客户端,或者将 apk 文件上传到加固服务的管理后台进行加固,然后等着加固完成,再下载安装包文件。
再次引用我的名言:
时间是最宝贵的财富,我们的时间得用在刀刃上。
本文基于 使用 Gradle 实现一套代码开发多个应用 中的 Gradle 配置进行迭代开发,带领大家实现 360加固
的自动化 Gradle 脚本。
我们的目标是全自动化,并且在每个团队成员的电脑上都能够实现一行命令执行,不需要做额外的配置。
完整的 config.gradle
配置:
1 | ext { |
新建一个 jiagu.gradle
文件:
1 | import org.apache.tools.ant.taskdefs.condition.Os |
执行 download360jiagu
就可以自动下载并解压 360 的加固程序啦。
1 | import org.apache.tools.ant.taskdefs.condition.Os |
现在我们只需要在命令行执行 ./gradlew releaseApp1
就可以静待输出了。
在根目录的 jiagu
文件夹中创建Channel.txt
文件,在其中可以配置你需要的多渠道信息。
如果需要配置更多的加固选项,可以在 jiagu/360jiagubao/jiagu/help.txt
中查看所有的加固命令。
我们的超管包不需要上传应用商店,直接加固上传到蒲公英,然后发送二维码给管理员下载安装。我们把自动加固和自动上传蒲公英整合到一起。
在jiagu.gradle
中添加单独加固超管包的方法:
1 |
|
修改蒲公英上传方法:
1 | def app1AdminFileDir = "${projectDir.parent}/jiagu/apk/app2Admin/" |
在命令行执行 ./gradlew uploadApp1Admin
就可以静待二维码地址输出。
如果你不喜欢执行命令行,我们只点一下鼠标也可以执行自动化命令:
时间是最宝贵的财富,我们的时间得用在刀刃上。
在文章 使用 Gradle 对应用进行个性化定制 中,我们能够针对一个应用的正式服、测试服、超管服等其他版本,进行个性化定制。所以经常会有测试跑过来说,帮我打个测试服的包吧、帮我打个正式服的包吧、帮我打个超管服的包吧。我打你一脸包😤😤🤯。
我们的目标是一行命令完成:
直接使用蒲公英的上传 API ,在 Gradle 中封装如下方法:
1 | private def uploadPGY(String filePath) { |
好了,执行以上方法就可以实现我们上面的两个目标啦。看不懂的我们继续,包教包会。
curl 是一种命令行工具,作用是发出网络请求,然后得到和提取数据,显示在”标准输出”(stdout)上面。
我们使用 curl 命令来调用蒲公英的接口上传 apk 文件。将蒲公英的 apiKey
和 uploadUrl
放在 config.gradle 中进行统一的管理。
1 | ext{ |
stdout
变量是用来获取网络请求返回的数据流,我们解析成 JSON 对象后打印出二维码地址和版本号信息。如果你还想上传或者输出更多信息请查看蒲公英的上传 API 。
这个时候大家要问了,apk 的文件路径从哪来呢?
我们有正式环境、测试环境和超管环境,并且每个环境生成的 apk 文件名也不同,那么我们怎么获取到 apk 文件的路径呢?
在config.gradle
中增加版本信息
1 | ext{ |
在根目录的 build.gradle
中增加获取 getApkName
方法
1 | def getTestVersionName(String suffix) { |
在 application 工程的 build.gradle
中修改 apk 文件名
1 | productFlavors { |
1 | def offlineFile = "${projectDir.absolutePath}/build/outputs/apk/offline/release/${getApkName(getTestVersionName("offline"))}" |
一行命令打3个包上传:./gradlew uploadOfflineApk uploadOnlineApk uploadAdminApk
我们的超管包是需要发给运营人员去使用的,防止泄露导致的安全风险,我们希望对超管包先进行加固然后再上传到蒲公英。
首先进行需求分析,确定技术方案:
整个业务流程如上,只不过我们为了效率和解耦,将每个处理逻辑独立开来使用多线程进行并发处理。
具体流程见下图:
下面的伪代码全部使用 kotlin
展示,不熟悉 kotlin
没关系,只需要关注具体的业务逻辑。
首先创建一个线程给 AudioRecord 进行录音采集:
1 | val emitter: FlowableProcessor<ShortArray> |
我们在子线程中读取到音频数据,并且通过 RxJava 将数据向下传递。(用什么传递不重要,重要的是将数据传递给下一层去进行处理)
外部接收 RxJava 的事件,对音频数据进行处理 (再次提醒,不需要在意细节,主要关注业务流程) :
1 | // 使用 observeOnIo() 操作将线程切换到 IO 线程 |
整个业务流程就是这样,我自己使用的手机和公司所有的测试机,试听录制出来的 MP3 文件都没有问题。
开开心心的打包,测试,上线。
然后你懂的,有些用户录制出现杂音、电流音、声音断断续续。😂😂
机智的同学可能通过标题已经猜到了问题的原因,但我当时没有手机进行问题复现,为了解决这个问题可是花了很大的功夫才定位到问题所在。
因为我们在录音采集时将数据读取到 buffer
对象中,然后将 buffer
对象通过 RxJava 向下传递,因为 RxJava 的下游都开启了异步线程去处理事件,那么在录音采集的死循环中不等当前的数据进行 MP3 编码完毕就对 buffer
对象写入新采集到的音频数据,这个时候 MP3 编码出来的音频数据就被污染了。
1 | val emitter: FlowableProcessor<ShortArray> |
要解决这个问题很简单:
1 | // 将 buffer 数据 copy 一份进行传递,这样就不会修改下游的数据了 |
但是使用 copy 的方式会频繁的创建、销毁 ShortArray 对象,能不能优化一下呢?
我们可以使用对象池来管理 ShortArray,这样就不会频繁的进行创建、销毁操作。在 Android 的 support.v4 包中有一个 Pools
类实现了简单的对象池功能:
1 | val bufferPool = Pools.SynchronizedPool<ShortArray>(10) |
很简单的一个多线程并发问题,但是当我们自己不能复现的时候,还是带来了很大的麻烦。
这种问题在编写 emitter.onNext(buffer)
这行代码的时候就应该要考虑到线程安全问题,并且我之前做直播截屏的时候也遇到过类似的问题,截取直播流的画面帧保存为图片,因为截屏的操作不会很频繁,当时是直接 copy 一份画面帧的数据保存为图片。
可是以前没有写博客记录这种小问题,导致遇到类似的问题尽量不记得了。所以这次记录下来😂😂。
需求:“将某个应用换一套皮肤、第三方账号、后台服务器,改个名字上线,并且以后的新功能同步进行更新”。
当你遇到这样的需求会怎么做呢?
是将项目复制一份,然后修改其中的内容,有新功能的时候再手动复制过来稍微修改一下 UI?
或者可以切换一个分支,在这个分支上修改相关的信息,每次开发完新功能,将代码合并过来,再稍微修改新功能的 UI?
现在我来介绍使用 Gradle
的 flavorDimensions
,实现一份代码构建多个应用。
老规矩,先上完整的 Gradle
配置:
1 | android { |
1 | ext { |
在上一篇文章的配置上进行了一些修改,同时保留上一篇文章里所有的功能。
首先来看最重要的一个概念:
1 | flavorDimensions "APP", "SERVER" |
这一行代码配置了两个维度的 flavor
,APP
代表多应用,SERVER
代表服务器版本。
根据上面的配置信息可以看到,app1
、app2
设置了 dimension "APP"
所以属于 APP
这个维度,offline
、online
、admin
设置了 dimension "SERVER"
属于 SERVER
这个维度。
根据 Product Flavors 的两个维度 APP [app1, app2] 和 SERVER [offline, online, admin] 以及 Build Type [debug, release],最后会生成以下 Build Variant:
app1AdminDebug
app1AdminRelease
app1OfflineDebug
app1OfflineRelease
app1OnlineDebug
app1OnlineRelease
app2AdminDebug
app2AdminRelease
app2OfflineDebug
app2OfflineRelease
app2OnlineDebug
app2OnlineRelease
是不是每个应用都有 3 个服务器版本,每个版本都有 debug
和 release
包。
我们要实现多应用,必须能安装在同一台手机上。所以不同应用之间的包名得不一样。
在 APP
维度的 flavor
中设置不同的 applicationId
,就可以实现修改应用包名。
1 | app1{ |
这样配置后,app1
和 app2
就能够安装在同一台手机上,也能同时上传应用商店。
有一点大家切记,AndroidManifest.xml
中的 package
不需要去修改,R 文件的路径是根据这个 package
来生成的。如果对 package
进行修改,R 文件的路径也会改变,所有引用到 R 文件的类都需要进行修改。
既然每个 Build Variant 都是由不同维度的 Product Flavors 和 Build Type 组合而来,我们肯定不能像上一篇文章一样将服务器的 URL 配置在 offline
、online
、admin
中了,因为 app1Offline
和 app2Offline
同样是测试服,但不是同一个应用 URL 也不一样。
这个时候就需要通过 task 操作来根据不同的组合设置不同的数据了。
1 | android.applicationVariants.all { variant -> |
两个 APP 的服务器 URL 和版本号不一致,所以通过 task 来动态设置。
不同的应用配置自己的应用名:
1 | resValue "string", "app_name", "APP1" |
这行代码的意思和在 strings.xml
中定义一个 String 值是一样的。不过这里通过 Gradle 配置了 app_name
就不能在 strings.xml
中再定义了,会报错提示有冲突。
如果多个应用使用同一个签名文件,按照上一篇文章写的在 buildTypes
的 release
和 debug
中配置就可以。但是每个应用的签名文件不一样呢?
1 | signingConfigs { |
配置多个签名文件,在 APP
这个维度的 flavor
中配置签名信息:
1 | app1{ |
这样就可以针对不同的应用设置不同的签名文件了。但是,还有一个要注意的地方,这个坑我以前没填上,而是绕远路绕过去了,现在我来填上它!
1 | debug { |
一定要在 debug
中将签名文件的配置置空,不然 Build Type 的权限比 Product Flavors 要高,而 debug
Build Type(构建类型) 会自动使用 debug
SigningConfig (签名配置),这样一来就将 flavor
中配置的签名信息给覆盖掉了。导致的问题就是编译 release
包没有问题,编译 debug
包就不能使用某些需要校验签名的第三方SDK了。
终于来到重头戏了,现在只需要更换 UI、文案或者某些界面布局和逻辑代码就大功告成啦。
首先,建立每个应用对应的 sourceSets
目录,比如:
sourceSets
位置是 src/app1/
sourceSets
位置是 src/app2/
app1
是已经开发完成的应用,只需要换 UI、文案就成了 app2
,在 src/app2/
目录下再新建 res
目录,将需要替换的切图命名和 app1
中的命名保持一致放入 res
对应的目录下就完美换肤了。
文案同理,将需要替换的字符串在 src/app2/res/values/strings.xml
中再写一份,保持 name
相同,其中的内容随便替换。
布局文件、style、color 替换的规则同上。
微信登录、分享、支付的回调是返回到 {应用包名.wxapi.WXEntryActivity}
、{应用包名.wxapi.WXPayEntryActivity}
这两个 Activity。
我们在 app1
和 app2
中都放入这两个回调 Activity:
然后在 AndroidManifest.xml
文件中动态配置 Activity 的包名:
1 | <!-- 微信分享回调 --> |
APPLICATIONID
占位符在 Gradle 中设置:
1 | manifestPlaceholders = [APPLICATIONID : applicationId] |
如果使用了 ShareSDK
做第三方分享和登录,需要配置 ShareSDK.xml
放到 assets
文件夹下,将 main/assets/ShareSDK.xml
复制一份到 app2/assets/ShareSDK.xml
,将里面的第三方 APP ID 和 APP KEY 替换一下就可以了。
项目如果使用了 ContentProvider
要注意替换 authorities
,如果 authorities
里面的值是一样的,手机上只能装一个应用哦,可以和上面动态配置 Activity 包名一样操作,用信鸽 SDK 演示一下:
1 | <!-- 【必须】 【注意】authorities修改为 包名.AUTH_XGPUSH, 如demo的包名为:com.qq.xgdemo --> |
上面的内容基本涉及到所有的方面,其他的细节也好,特殊的需求定制也好,使用上面的方式去处理都能够解决。希望大家不要光学会复制粘贴,要掌握其原理,遇到类似的需求就能举一反三。
总结一下技术点:
manifestPlaceholders
-> AndroidManifest.xml
占位符buildConfigField
-> BuildConfig
动态配置常量值resValue
-> String.xml
动态配置字符串signingConfigs
-> 配置签名文件productFlavors
-> 产品定制多版本flavorDimensions
-> 为产品定制设置多个维度android.applicationVariants
-> 操作 taskproductFlavors
,有同学评论在 Android Studio 3.0 上编译不了。官方文档:
简单解释一下,'com.android.tools.build:gradle:3.0.0-alpha5'
插件 3.0.0 版本包含一个新的依赖机制,强制所有的 flavor
必须配置一个 flavor dimension
。
在上一篇文章的基础上,稍作修改:
1 | //配置一个默认的 flavorDimensions |
主要就是给 flavor
设置默认的 Dimension
,这样编译就没有问题了。
Gradle
对应用的不同版本进行个性化定制。超管服务器
专供运营人员使用,对应用内的一些内容进行监管,具有一些管理员才有的操作权限。管理员
文字,线上包则正常显示版本号。versionCode
自增,避免发版时忘记手动修改导致老版本不能覆盖安装。admin
,日常运行的 debug
包渠道名为 test
,上线的包使用加固软件进行多渠道加固。debug
包和 release
包使用同样的签名,避免直接运行的 debug
包因为签名问题不能使用需要校验签名的第三方服务,比如:QQ 登录,微信登录,高德地图。debug
包打印日志信息,release
包不打印日志信息以上某些场景从我工作以来就一直存在,以前用 eclipse
开发时除了每次都手动去修改一些开关变量也没啥好办法,可能是因为当时菜 ╮(╯▽╰)╭(如果你们有什么好方法的话)。后来切换到 Android Studio
后使用 Gradle
进行依赖管理已经让人很是欣喜,既然如此能不能使用 Gradle
将以上问题统统解决,完全自动化呢?答案是:必须的。
先上完整的 Gradle
配置。
1 | android { |
根目录下的 build.gradle
文件进行如下配置,主要是将版本号和测试包的序号抽取出来:
1 | ext { |
下面就根据场景来依次介绍对应的配置代码。
这里创建了三个 flavor,分别是 offline 测试服
、online 正式服
、admin 超管服
。并且通过 buildConfigField
动态配置服务器的 URL 常量值到编译后自动生成的 BuildConfig
类中。
图中可以看到,不止有 DOMAIN_NAME
常量值,还有一个 FLAVOR
常量。这个 FLAVOR
常量中的值是 offline
,代表当前在 offline
这个版本上面。那怎么切换到其他的服务器呢?
点开左下角的 Build Variants
, 可以自由切换当前运行的版本。需要在管理员包中开启一些高级的功能,可以判断 FLAVOR
的值是不是 admin
,如果是的话就显示管理员的操作布局。当然必不可少的要对用户权限进行校验哦。
versionName
大家看上图 BuildConfig
类中 VERSION_NAME
常量的值为 2.0.2.0001-debug
,当前是测试服的 debug
包,所以 versionName
应该是正常的 2.0.2 版本后面拼上当前出包的序号 0001 ,再拼上 debug
的后缀,所以完整的版本号是 2.0.2.0001-debug
。
看看不同服务器版本的 VERSION_NAME
:
offlineRelease
版本为 2.0.2.0001
offlineDebug
版本为 2.0.2.0001-debug
adminRelease
版本为 2.0.2-管理员
adminDebug
版本为 2.0.2-管理员-debug
onlineRelease
版本为 2.0.2
onlineDebug
版本为 2.0.2-debug
如果我们接口需要上传版本号给服务器呢?肯定不能直接上传这些定制化后的 VERSION_NAME
,那么我们在 Gradle
中增加一个 buildConfigField
将原始的版本号存起来就好了。
1 | buildConfigField "String", "versionNumber", "\"${rootProject.ext.versionName}\"" |
versionCode
自增这里采用了主流的方式,使用 git
的 commit
次数作为 versionCode
的值。不用担心这个值会超过 int
的上限,你得敲烂多少键盘才能提交 2147483648次 commit
。
1 | static int gitVersionCode() { |
Gradle
多渠道打包的文章太多了,相关的基础我就不讲了。简单讲下本文相关的配置吧。
通过定义 manifestPlaceholders
键值对,在 AndroidManifest.xml
文件中使用占位符的方式动态输入 UMENG_APPKEY
和 UMENG_CHANNEL
。
然后在 debug
的 buildType
中修改渠道名为 test
,在 admin
的 Flavor
中修改渠道名为 admin
。如果选择 adminDebug
版本,则渠道名为 test
,buildType
中的配置会覆盖掉 Flavor
中的配置。
由于我们线上使用第三方加固,所以多渠道包就交给第三方加固软件来生成了。
关于多渠道打包我还有两句话要说,以前使用 Gradle
进行多渠道打包,通过代码自定义修改 apk
文件的输出路径,Android Studio
编译的时候时不时的报一些文件找不到的错误,以前都是通过在 Gradle
文件中随便修改一点东西然后刷新一下 Gradle
文件来解决。现在我打包不修改输出路径,再也没遇到以前的那些问题了。
建议大家使用 assemble
命令来进行打包,比如我要出一个测试包使用 ./gradlew assembleOfflineRelease
命令,apk
文件生成在 /build/outputs/apk/
目录下。直接执行 assemble
命令是编译 Build Variants
中的所有包,如果你要编译指定版本的包,直接在 assemble
命令后面拼上指定的 Build Variant
就好了。
debug
包使用 release
签名这个问题在 eclipse
时代,可以直接在设置里面配置 debug
签名文件为 release
的签名文件。
用 Android Studio
只需要在 Gradle
中配置就好了。
首先配置签名文件的信息:
1 | signingConfigs { |
然后在 buildTypes
中设置签名信息:
1 | buildTypes { |
这个太简单了,不想单独列出来。不过上面场景里面提出来了,就简单一行代码展示吧。
1 | release { |
在日志工具类中使用 BuildConfig
类中的 LOG_DEBUG
常量来判断当前是否应该输出日志。
当然也可以用这个开关来控制开启严格模式等其他只适合在 debug
模式下开启的设置。
唧唧歪歪说了这么多,在懂的人眼中自然很简单,在对 gradle
一点都不了解的人眼中就可以直接复制过去用了。当然我是不建议直接复制,毕竟需求稍微一改,你可能就束手无策了。建议大家还是以理解为主,掌握其原理自然一通百通。
Android Studio 3.0 上 Gradle 改动
本文已授权微信公众号:鸿洋(hongyangAndroid)原创首发。
本文主要讲解 Lottie 库动态加载 SD 卡上带图片资源的动画,并对各种机型做全屏适配。
Lottie 的优点:
跨平台,支持 Android、iOS、React Native 平台
支持实时渲染 After Effects 动画,让 app 加载动画像加载图片一样简单。
资源动态下载,减小 APP 体积,上线新的动画效果不需要发版
更多优点等你发现
做直播软件肯定少不了各种礼物的动画效果,当上线新的礼物时,不仅 Android、ios 客户端需要实现新的动画效果,还很难兼容老版本。
平常过节日的时候,很多 APP 都会做各种活动,修改启动页的图片,更改应用内的按钮图标,如果涉及到动画,那么肯定需要提前一两个版本将节日的动画实现代码预制到应用内
更多应用场景等你探索
现在有了 Lottie,可以让设计师使用 After Effects 进行动画设计,通过 Bodymovin 插件导出 json 文件,将动画资源打包上传到服务器后,客户端通过动态下载资源文件来执行动画。这样上线新的礼物,只需要将资源文件上传,客户端不需要发版完全可以执行新礼物的动画效果。流程如下:
本文就以直播间播放动画为例子来讲解具体的实现方案,先看下动画效果:
直播软件的大礼物一般都是飞机、跑车、航母、花瓣雨等,这些物品不是简单的线条、色块所能绘制,所以使用图片文件来实现动画效果。
导出的动画资源包括一个 json 文件和一组图片文件:
将这些文件打成压缩包上传到后台,客户端下载压缩包进行解压,使用 Lottie 加载本地资源执行动画:
1 | File jsonFile = new File(giftDir, "79.json"); |
关键的代码就是设置图片资源代理,去 SD 卡解析图片文件,那么怎么知道该解析哪一张图片呢?咱们来看看 json 文件里面的内容:
assets
字段是图片资源的数组,具体的解析的源码如下:
1 | private LottieImageAsset(int width, int height, String id, String fileName) { |
直接根据 ImageAssetDelegate
代理类的 fetchBitmap(LottieImageAsset asset)
方法中的 LottieImageAsset
参数获取当前需要解析的图片文件名,去 images 文件夹下面解析对应的文件就OK啦。
这几行代码就实现了从SD卡动态加载动画,那么这样就算完工了吗?看看上面的动画是不是感觉有什么地方不对劲?好吧,作为一个 Android 软件工程师,一定要记住2个字 适配 适配 适配
动画是全屏的效果,小幽灵也是从屏幕外飞进来的没有问题,为什么背景图离屏幕两边有空隙呢?
再来看看这个动图,为什么隐藏虚拟按键就全屏了呢?
再来看看 json 文件里面的内容:
背景图的宽高和画布的宽高是一样的,那么为什么有虚拟按键的时候背景图就不全屏呢?原因其实很简单,来看一下 Lottie 是怎么解析 json 数据:
1 | static LottieComposition fromJsonSync(Resources res, JSONObject json) { |
解析出了动画的宽高、帧率等信息,这里将解析出来的宽高乘上了屏幕的像素密度,然后设置渲染区域的边界,为了便于理解,本文将其称为画布。我这台手机是 1080P 的分辨率,density = 3,scaledWidth = 2250,scaledHeight = 4002,现在缩放后的画布宽高比手机屏幕大了太多,如果动画在这种尺寸下进行渲染肯定不行。所以 LottieAnimationView 加载 Composition 时判断了画布的宽高如果大于手机屏幕的宽高就进行等比例缩小:
1 | public void setComposition(@NonNull LottieComposition composition) { |
计算出宽和高的缩放比后,为了让画布小于屏幕,所以取较小的一个比例,调用 setScale 方法将缩放比设置到 lottieDrawable 上:
1 | public void setScale(float scale) { |
lottieDrawable 的 setScale 方法保存了缩放比,并且更新了绘制的矩形范围:
1 | public void setScale(float scale) { |
这里可以看到矩形的范围是根据画布的宽高进行了等比例的缩放
1 | private void updateBounds() { |
现在画布被缩放了,然而背景图呢?来看一下 lottieDrawable 的绘制代码:
1 | public void draw(@NonNull Canvas canvas) { |
lottieDrawable 在绘制的时候对 matrix 设置了缩放比,然后调用了 compositionLayer 去进行具体的绘制。这个 compositionLayer 就是所有图层的一个组合,它有一个List<BaseLayer> layers
属性, 这个属性就是 json 文件里面的layers
节点解析出来的图层列表,每个图层中间还包含一些属性动画。compositionLayer.draw(canvas, matrix, alpha)
方法中主要调用了 drawLayer 抽象方法由图层的具体实现类执行绘制:
1 | void drawLayer(Canvas canvas, Matrix parentMatrix, int parentAlpha) { |
可以看到 CompositionLayer 类的 drawLayer 方法遍历 layers 集合进行循环绘制,这里是使用图片文件做的动画,对应的 Layer 实现类为 ImageLayer。 看下 ImageLayer 的绘制方法:
1 | public void drawLayer(@NonNull Canvas canvas, Matrix parentMatrix, int parentAlpha) { |
getBitmap() 方法会调用到一开始设置的代理类 ImageAssetDelegate ,从 SD 卡加载图片。
代码中调用了 canvas 的save
、restore
方法来进行图层的叠加绘制,从 lottieDrawable 的draw
方法传递下来的matrix
用到了concat
方法上,对 bitmap 进行了等比缩放。
整个流程跑下来,Lottie 库的动画渲染机制已经基本了解,背景图没有全屏展示的原因如下:
背景图的长宽比是 16 : 9,手机屏幕的长宽比也是 16 : 9,但是因为底部的虚拟按键占了一部分的高度,屏幕可用空间的长宽比大约为 3 : 2,所以导致背景图不能铺满屏幕
动画不能全屏有两种情况,一种是手机长宽比和画布的长宽比是相同的,但是状态栏、导航栏占了屏幕一部分空间导致不能全屏,使用方案一可以解决问题。还有一种情况是手机屏幕长宽比和画布的长宽比就是不一样,毕竟 Android 机型这么多,有几台奇葩手机很正常,那么使用方案二可以实现全屏。
在执行动画的界面隐藏虚拟按键,或者将虚拟按键设置为透明浮在布局上面,这样屏幕的长宽比和画布的长宽比一样就没有问题。目前市面上的手机基本上都是 720P、1080P、2K 等分辨率,这些分辨率都是 16 : 9 的尺寸。
状态栏和虚拟按键透明悬浮在布局上面,设置样式:
1 | <style name="Theme"> |
隐藏虚拟按键通过代码设置:
1 | Window window = getWindow(); |
使用代码隐藏虚拟按键需要注意一点:界面的切换会导致 setSystemUiVisibility() 的设置被清空,最好是在 onResume() 或者 onWindowFocusChanged() 方法中进行设置。
如果要适配其他长宽比的屏幕,咋办呢?两行代码解决问题,只不过图片有一部分会被裁剪。设置控件的宽高为match_parent
,设置android:scaleType
为centerCrop
1 | <com.airbnb.lottie.LottieAnimationView |
Lottie 发布才几个月,很多功能还不够完善,缓存机制也比较弱,像这种从 SD 卡动态加载的方式,需要自己去实现缓存逻辑。但是这点小瑕疵掩盖不了牛逼的事实,就目前这个需求来说,已经大大的降低了开发成本。只不过设计师们需要好好练练 AE 了,动画炫不炫就看设计师给不给力啦😆😆
本文是作者的处女作,如果对大家有帮助,希望大家多多支持,给予作者更多的创作动力,提供更好的作品给大家。
1 | $ hexo new "My New Post" |
More info: Writing
1 | $ hexo server |
More info: Server
1 | $ hexo generate |
More info: Generating
1 | $ hexo deploy |
More info: Deployment
]]>