属性动画系统是一个强健的框架,用于为几乎任何内容添加动画效果。您能够定义一个随时间更改任何对象属性的动画,不管其是否绘制到屏幕上。属性动画会在指定时长内更改属性(对象中的字段)的值。要添加动画效果,请指定要添加动画效果的对象属性,例如对象在屏幕上的位置、动画效果持续多长时间以及要在哪些值之间添加动画效果。android
首先,让咱们经过一个简单的示例来了解动画的工做原理。图 1 描绘了一个假设的对象,该对象的 x 属性(表示其在屏幕上的水平位置)添加了动画效果。动画时长设置为 40 毫秒,要移动的距离为 40 像素。该对象每隔 10 毫秒(这是默认的帧刷新频率)会水平移动 10 像素。在 40 毫秒时,动画中止,同时对象在水平位置 40 处中止。这是使用线性插值(表示对象以恒定速度移动)的动画示例。程序员
类图数据库
官方文档:developer.android.comjson
项目开发中效率都是很是重要的一个指标,一样一个功能别的团队须要一周完成,但大家团队只须要3天,那毫无疑问大家团队的效率就远远高于其余团队,如何提升效率是每一个团队都在极力追求的目标,低代码就是提升效率的一个重要方向之一,属性动画在咱们平常开发过程当中每一个动画都须要编写逻辑,人工效率和动画复杂度成正比,开发效率亟待提升。api
这里说的低代码不是如今各类低代码平台的一些概念,有些相似传统观念中的组件化的思路,简单理解其实就是经过一些基础能力的沉淀,尽量减小开发者代码编写,将重复的工做标准化,标准化带来最直观的就是效率的提升数组
其实在咱们开发过程当中都在践行着低代码的原则,好比咱们开发一个功能A,逻辑中包含网络请求、图片加载、数据库管理操做等,而后开发一个功能B,逻辑中也有网络请求、图片加载、数据库管理这些模块,前期由于设计经验不足可能都是单独有相关功能就开发相关功能缓存
但随着设计经验的丰富,咱们对于相同的逻模块就会考虑抽象出来造成公共组件,好比网络模块、图片加载模块、数据库管理模块等,这其实就是低代码的一个实现markdown
下面的这种设计就是低代码的一个实现,将重复的功能抽象出来提供给后续其余模块直接使用,避免重复造轮子,开发流程就是B组件只须要开发核心功能便可,其余的网络请求、图片加载、数据库管理直接使用公共组件,直接带来的就是团队开发效率的提高网络
就属性动画这块来讲怎么实现低代码呢?首先咱们要找到开发中有哪些流程是重复且耗时的,而后经过设计方案去实现标准化、自动化,减小须要人工参与的流程,这样的方案必然会遵循低代码的原则数据结构
咱们先来看下目前属性动画的一个工做流
相似这种设计给出的动画说明描述文件相信你们开发过程当中应该常常见到
流程中存在的问题
要解决上诉的三个问题对方案的要求就有三个
看到这里有没有似成相识的感受?这个不就是Lottie吗?对!Lottie的出现就是为了解决动画开发过程的上述那些问题的。
上述那些咱们在作动画开发的时候遇到的问题,Lottie的出现都帮咱们解决了,因此Lottie就是一个很好的动画低代码解决方案。
可是Lottie能解决的只是展现型动画,是和业务不相关的纯展现型动画,好比一个跳动的icon,一些新手引导交互动画,这些动画和咱们的业务是剥离开来的,无需和业务有关联,这种类型的动画咱们均可以交给Lottie来实现
像下面这种:
而若是是和业务相关的组件须要动画则Lottie是没法支持的,好比咱们的一个按钮须要有缩放效果,一个卡片须要有渐隐渐现循环显示效果。
虽然没法使用Lottie来实现,可是咱们能够参考Lottie的方案,来设计咱们的属性动画低代码方案,咱们先看下Lottie实现原理
Lottie是将AE制做的动画文件最终组合成一个动态的Imageview渲染出来,实现了所见即所得,AE导出动画文件,端侧经过Lottie框架播放动画
若是咱们要参考Lottie的方案实现上述属性动画,则对于设计师来讲也只是在AE中设计,而后导出动画文件,端侧直接播放动画文件来实现动画效果
基于此咱们能够设计属性动画的方案,经过AE插件导出动画数据,而后解析出属性动画相关的数据,再自动封装成对应的属性动画,最后绑定到业务控件上进行播放,从而实现了属性动画的低代码,解决了业务开发中属性动画的痛点问题
核心原理其实至关于AE里面编辑的是动画的模板,而后将模板挂载到控件上,在咱们的方案中动画就至关于一个脚本挂件,一个控件能够挂不一样的挂件展现不一样的动画效果
工做流
端侧调用
AnimationManager
.getInstance()
.playAnimation(button, "data.json", "btn_scale");
复制代码
参数说明:
button : 须要播放属性动画的控件,这个控件能够是任何自定义的view
data.json : 动画文件名(同Lottie)
btn_scale : 须要播放的动画,同一个动画文件里面能够有多组动画,这个和lottie有区别
简单介绍下源码,核心原理和Lottie同样都是将动画数据转换成可执行渲染逻辑,Lottie是将动画数据转换成可执行帧数据,而后在渲染时候按照数据绘制,咱们这个属性动画就是将动画数据转换成可执行属性动画,除了属性解析层逻辑外,底层的解析以及工具类都是参考的Lottie源码,能够理解为站在Lottie的肩膀上扩展出的咱们这个方案。(源码都作了删减)
先贴一下动画文件的数据结构方便你们有个清晰的认识,下面一段是AE经过bodymovin导出的动画文件精简版
{
"v": "5.7.8",
"fr": 60,
"ip": 0,
"op": 180,
"w": 400,
"h": 200,
"nm": "测试",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 4,
"ty": 4,
"nm": "btn_scale", //动画名称,对应api里面的animationName
"sr": 1,
"ks": { //关键帧数据,咱们须要解析的
"o": { //o 标识透明度变换
...
},
"r": { //r 标识旋转变换
...
},
"p": { //p 位移变换
...
},
"a": { // 锚点变换,这个属性里面能够忽略
...
},
"s": { //缩放变换
"a": 1,
"k": [ //关键帧数据
{
"i": {
"x": [],
"y": []
},
"o": {
"x": [],
"y": []
},
"t": 0, //开始帧
"s": [ //对应的数据,这里是缩放下面的则s[0]标识x缩放s[1]标识y缩放
100,
100,
100
]
},
{
"i": {
"x": [],
"y": []
},
"o": {
"x": [],
"y": []
},
"t": 30, //第二个关键帧
"s": [
110,
110,
100
]
},
],
}
},
...
}
]
}
复制代码
传入动画文件名称须要本地解析成属性数据存到内存中,所以须要对资源进行一些IO操做
相关类:
AnimationManager 管理动画入口
AnimationViewWrapper 包装动画组件
AttributeCompositionFactory 动画资源读取
核心逻辑入口,提供两个方法一个是播放默认动画,一个是播放制定动画,一个动画文件里面能够包含多组动画,经过动画名称区分
public class AnimationManager {
private final static String DEFAULT_ANIMATION = "animation";
/** * 播放默认动画 * @param target 目标控件 * @param assetName 动画资源名称 */
public DynamicComponent playAnimation(View target, String assetName) {
return playAnimation(target, assetName, DEFAULT_ANIMATION);
}
/** * 播放指定名称动画 * @param target 目标控件 * @param assetName 动画资源名称 * @param animationName 动画名称 */
public DynamicComponent playAnimation(View target, String assetName, String animationName) {
DynamicComponent viewWrapper = new AnimationViewWrapper(target, assetName, animationName);
viewWrapper.playAnimation();
return viewWrapper;
}
}
复制代码
动画控件的一个包装类,实现动态组件的一些方法包含动画的一些控制流程
/** * 初始化 * @param target 绑定的控件 * @param assetName 动画文件名称 * @param animationName 动画名称 */
public AnimationViewWrapper(final View target, final String assetName, String animationName) {
this.target = target;
this.assetName = assetName;
this.animationName = animationName;
setCompositionTask(fromAssets(assetName));
}
/** * 从asset里面加载资源 * @param assetName 动画资源名称 * @return AnimationTask */
private AnimationTask<AttributeComposition> fromAssets(final String assetName) {
return cacheComposition ?
AttributeCompositionFactory.fromAsset(target.getContext(), assetName) :
AttributeCompositionFactory.fromAsset(target.getContext(), assetName, null);
}
}
复制代码
属性组件工厂类,提供经过本地文件&网络建立AnimationTask,以及缓存逻辑
private static AnimationResult<AttributeComposition> fromJsonReaderSyncInternal( JsonReader reader, @Nullable String cacheKey, boolean close) {
...
//属性解析 核心逻辑
AttributeComposition composition = AttributeCompositionParser.parse(reader);
if (cacheKey != null) {
AttributeCompositionCache.getInstance().put(cacheKey, composition);
}
return new AnimationResult<>(composition);
...
}
/** * 缓存处理 * @param cacheKey 缓存key * @param callable * @return AnimationTask */
private static AnimationTask<AttributeComposition> cache( @Nullable final String cacheKey, Callable<AnimationResult<AttributeComposition>> callable) {
......
return task;
}
复制代码
AE输出的动画是按图层归类的,须要咱们将对应的图层动画信息解析并分类存储
相关类:
AttributeCompositionParser 最外层疏忽解析
AttributeLayerLayerParser 层关键帧数据解析
第一层解析类,解析第一层数据
public static AttributeComposition parse(JsonReader reader) throws IOException {
......
reader.beginObject();
while (reader.hasNext()) {
switch (reader.selectName(NAMES)) {
case 0:
case 1:
reader.nextInt();
break;
case 2:
case 3:
reader.nextDouble();
break;
case 4:
frameRate = (float) reader.nextDouble();
break;
case 5:
String version = reader.nextString();
break;
case 6:
//全部的属性动画都在这个层级下面
parseLayers(reader, attributeAnimationInfoMap);
break;
default:
reader.skipName();
reader.skipValue();
}
}
//将帧数据转换成时间
frameConvertToTime(frameRate, attributeAnimationInfoMap);
//初始化composition
composition.init(frameRate, attributeAnimationInfoMap);
return composition;
}
复制代码
解析核心ks关键帧层数据
public static AttributeLayer parse(JsonReader reader) throws IOException {
......
reader.beginObject();
while (reader.hasNext()) {
switch (reader.selectName(NAMES)) {
case 0:
layerName = reader.nextString();
break;
case 1:
layerId = reader.nextInt();
break;
case 2:
refId = reader.nextString();
break;
case 3:
//属性数据解析核心逻辑
transform = AttributeTransformParser.parse(reader);
break;
default:
reader.skipName();
reader.skipValue();
}
}
reader.endObject();
return new AttributeLayer(layerName, layerId, refId, transform);
}
复制代码
AE输出的是动画中的转换操做数据,须要转换成咱们端侧须要的属性数据
相关类:
AttributeTransformParser 属性转换数据解析
AttributeValueParser 属性解析基类
AlphaValueParser 透明度属性解析
TranslationValueParser 位移属性解析
RotationValueParser 旋转属性解析
ScaleValueParser 缩放属性解析
public static AttributeTransform parse(JsonReader reader) throws IOException {
while (reader.hasNext()) {
switch (reader.selectName(NAMES)) {
case 0: // p 解析位移变换
translationInfos = AttributeValueParser.parseTranslation(reader);
break;
case 1: // s 解析缩放变换
scaleInfos = AttributeValueParser.parseScale(reader);
break;
case 2: // r 解析旋转变换
rotationInfos = AttributeValueParser.parseRotation(reader);
break;
case 3: // o 解析透明度变换
alphaInfos = AttributeValueParser.parseAlpha(reader);
break;
default:
reader.skipName();
reader.skipValue();
}
}
return new AttributeTransform(translationInfos, rotationInfos, alphaInfos, scaleInfos);
}
复制代码
AE输出的是关键帧动画信息,咱们须要将关键帧信息转换成属性数据组
AE输出的单位是帧索引,须要将帧索引转换成时间单位毫秒
根据帧数组新,封装成动画列表
获取封装好的动画列表进行播放
/** * 获取AnimatorSet列表 * @param target * @param animation * @return List<AnimatorSet> */
public List<AnimatorSetWrapper> getAnimatorSetList(final View target, final String animation) {
AttributeTransform transform = attributeAnimationInfoMap.get(animation);
if (transform == null) {
return null;
}
List<AnimatorSetWrapper> animatorSets = new ArrayList<>();
//解析缩放变换
AnimatorSetParserHelper.parseScale(target, animatorSets, transform);
//解析旋转变换
AnimatorSetParserHelper.parseRotation(target, animatorSets, transform);
//解析透明度变换
AnimatorSetParserHelper.parseAlpha(target, animatorSets, transform);
//解析平移变换
AnimatorSetParserHelper.parseTranslation(target, animatorSets, transform);
//按动画时间排序
Collections.sort(animatorSets, new Comparator<AnimatorSetWrapper>() {
@Override
public int compare(AnimatorSetWrapper o1, AnimatorSetWrapper o2) {
return o2.getDuration() - o1.getDuration();
}
});
return animatorSets;
}
/** * 开始动画 */
public void startAnimation() {
for (AnimatorSetWrapper setWrapper : animatorSetList) {
AnimatorSet animatorSet = setWrapper.getAnimatorSet();
if (setWrapper.getAnimatorSet().isRunning()) {
animatorSet.cancel();
}
animatorSet.start();
}
}
复制代码
生产力的提升是咱们程序员最期盼的目标,好钢要用在刀刃上,咱们要拒绝重复且无心义的工做,只有这样才有更多的时间用来提升本身,所以在工做中咱们要直面效率瓶颈,发现问题不要妥协,不要绕开,更不要让本身陷入低效的循环中去,咱们要把问题抛出来,你们一块儿讨论优化的方案,就如同本篇文章提到的属性动画对于移动端来讲就是重复且无心义的工做,写1000行和写10行对技术能力并无任何提升,浪费的只是咱们宝贵的学习时间,但愿这边文章可以给你的工做中带来一些帮助,有疑问欢迎留言一块儿讨论,谢谢!
hi, 我是快手电商的HD
快手电商无线技术团队正在招贤纳士🎉🎉🎉! 咱们是公司的核心业务线, 这里云集了各路高手, 也充满了机会与挑战. 伴随着业务的高速发展, 团队也在快速扩张. 欢迎各位高手加入咱们, 一块儿创造世界级的电商产品~
热招岗位: Android/iOS 高级开发, Android/iOS 专家, Java 架构师, 产品经理(电商背景), 测试开发... 大量 HC 等你来呦~
内部推荐请发简历至 >>>咱们的邮箱: hr.ec@kuaishou.com <<<, 备注个人花名成功率更高哦~ 😘