屬性動畫系統是一個強健的框架,用於爲幾乎任何內容添加動畫效果。您能夠定義一個隨時間更改任何對象屬性的動畫,不管其是否繪製到屏幕上。屬性動畫會在指定時長內更改屬性(對象中的字段)的值。要添加動畫效果,請指定要添加動畫效果的對象屬性,例如對象在屏幕上的位置、動畫效果持續多長時間以及要在哪些值之間添加動畫效果。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 <<<, 備註個人花名成功率更高哦~ 😘