踩坑之路:Flutter Lottie動(dòng)畫組件踩坑

背景

Flutter中我使用的是這個(gè)Lottie組件
我在做一個(gè)點(diǎn)贊的動(dòng)畫的時(shí)候杠茬,就是很簡(jiǎn)單的將like.json放到assets目錄下踏烙,然后再同一個(gè)目錄下面創(chuàng)了了一個(gè)images文件夾,文件夾下面放著like-fill.png圖片
注: 這么做的原因是json文件有下面這段代碼:

  "assets": [
    {
      "id": "image_0",
      "w": 120,
      "h": 120,
      "u": "images/",
      "p": "like-fill.png",
      "e": 0
    }
  ]

然后使用下面這段代碼加載lottie動(dòng)畫:

Lottie.asset(
               'assets/like.json',
                repeat: true,
                reverse: true,
                animate: true,
              ),

最終顯示結(jié)果如下方gif一樣:


異常點(diǎn)贊.gif

而正常的結(jié)果應(yīng)該是跟下面這個(gè)一樣


正常點(diǎn)贊.gif

顯然最終的效果是非常不符合預(yù)期的,異常點(diǎn)贊的小手變得特別大庸蔼。那么就得看下lottie組件到底做了什么事情

探究

下面是Lottie時(shí)序圖以及最終處理的代碼


Lottie時(shí)序圖1.jpg
  1. 創(chuàng)建LottieBuilder對(duì)象,
  2. 通過(guò)LottieBuilder對(duì)象創(chuàng)建AssetsLottie對(duì)象,主要根據(jù)生成的key值從cache中獲取已經(jīng)創(chuàng)建的Future<LottieComposition>
  3. 如果不存在那么先創(chuàng)建一個(gè)Future<LottieComposition>,然后加載Assets文件夾下對(duì)應(yīng)的Lottie文件
  4. 然后在FutureBuilder的build方法通過(guò)future獲取LottieComposition對(duì)象型酥,構(gòu)建Lottie的Widget


    Lottie時(shí)序圖2.jpg
  5. Lottie 的Widget創(chuàng)建RawLottie弥喉,然后創(chuàng)建RenderLottie由境,創(chuàng)建LottieDrawable, 創(chuàng)建CompositionLayer
  6. CompositionLayer根據(jù)傳入的Models創(chuàng)建不同的BaseLayer對(duì)象(點(diǎn)贊的圖是一個(gè)Image類型藻肄,所以創(chuàng)建的是ImageLayer)
  7. 由于RenderLottie是個(gè)RenderBox對(duì)象嘹屯,會(huì)執(zhí)行paint方法所以最終會(huì)執(zhí)行ImageLayer的draw方法

下面給出ImageLayer的Draw方法處理邏輯:

@override
  void drawLayer(Canvas canvas, Size size, Matrix4 parentMatrix,
      {int parentAlpha}) {
    var bitmap = getBitmap();
    if (bitmap == null) {
      return;
    }
    var density = window.devicePixelRatio;

    paint.setAlpha(parentAlpha);
    if (_colorFilterAnimation != null) {
      paint.colorFilter = _colorFilterAnimation.value;
    }
    canvas.save();
    canvas.transform(parentMatrix.storage);
    var src =
        Rect.fromLTWH(0, 0, bitmap.width.toDouble(), bitmap.height.toDouble());
    var dst = Rect.fromLTWH(
        0, 0, bitmap.width * density, bitmap.height.toDouble() * density);
    canvas.drawImageRect(bitmap, src, dst, paint);
    canvas.restore();
  }

很顯然從最終繪制的代碼看州弟,是調(diào)用了getBitmap獲取的bitmap婆翔,那么getbitmap做了什么操作呢啃奴?

  Image /*?*/ getBitmap() {
    var refId = layerModel.refId;
    return lottieDrawable.getImageAsset(refId);
  }
  ui.Image getImageAsset(String ref) {
    var imageAsset = composition.images[ref];
    if (imageAsset != null) {
      return imageAsset.loadedImage;
    } else {
      return null;
    }
  }

從上述代碼看,應(yīng)該是從Composition里面獲取的images數(shù)組依溯。Composition在哪里設(shè)置的呢瘟则?其實(shí)第一張時(shí)序圖里面已經(jīng)給出了地方(就是調(diào)用的LottieComposition的fromBytes方法獲取的)我們?cè)賮?lái)看下這個(gè)方法的主要邏輯:

  static Future<LottieComposition> fromByteData(ByteData data, {String name}) {
    return fromBytes(data.buffer.asUint8List(), name: name);
  }

  static Future<LottieComposition> fromBytes(Uint8List bytes,
      {String name}) async {
    Archive archive;
    if (bytes[0] == 0x50 && bytes[1] == 0x4B) {
      archive = ZipDecoder().decodeBytes(bytes);
      var jsonFile = archive.files.firstWhere((e) => e.name.endsWith('.json'));
      bytes = jsonFile.content as Uint8List;
    }

    var composition = LottieCompositionParser.parse(
        LottieComposition._(name), JsonReader.fromBytes(bytes));

    if (archive != null) {
      for (var image in composition.images.values) {
        var imagePath = p.posix.join(image.dirName, image.fileName);
        var found = archive.files.firstWhere(
            (f) => f.name.toLowerCase() == imagePath.toLowerCase(),
            orElse: () => null);
        if (found != null) {
          image.loadedImage = await loadImage(
              composition, image, MemoryImage(found.content as Uint8List));
        }
      }
    }

    return composition;
  }

發(fā)現(xiàn)是調(diào)用了LottieCompositionParser的parse方法獲取的醋拧,那么可想而知里面的操作應(yīng)該就是類似乎xml解析一樣的根據(jù)各種tag獲取對(duì)應(yīng)的值。

  static final JsonReaderOptions _names = JsonReaderOptions.of([
    'w', // 0
    'h', // 1
    'ip', // 2
    'op', // 3
    'fr', // 4
    'v', // 5
    'layers', // 6
    'assets', // 7
    'fonts', // 8
    'chars', // 9
    'markers' // 10
  ]);
  static LottieComposition parse(
      LottieComposition composition, JsonReader reader) {
    ...代碼省略...
    reader.beginObject();
    while (reader.hasNext()) {
      switch (reader.selectName(_names)) {
        ...代碼省略...
        case 7:
          _parseAssets(reader, composition, precomps, images);
          break;
       ...代碼省略
      }
    }
    ...代碼省略...
    return composition;
  }

我們那個(gè)點(diǎn)贊的圖片like-fill.png其實(shí)是在assets的字段中庆械,也就是reader.selectName(_names) == 7的場(chǎng)景干奢。所以會(huì)執(zhí)行_parseAssets方法:

 static final JsonReaderOptions _assetsNames = JsonReaderOptions.of([
    'id', // 0
    'layers', // 1
    'w', // 2
    'h', // 3
    'p', // 4
    'u' // 5
  ]);

  static void _parseAssets(JsonReader reader, LottieComposition composition,
      Map<String, List<Layer>> precomps, Map<String, LottieImageAsset> images) {
    reader.beginArray();
    while (reader.hasNext()) {
      // For images
      var width = 0;
      var height = 0;
      reader.beginObject();
      while (reader.hasNext()) {
        switch (reader.selectName(_assetsNames)) {
          ...代碼省略...
          case 2:
            width = reader.nextInt();
            break;
          case 3:
            height = reader.nextInt();
            break;
          ...代碼省略...
        }
      }
     ...代碼省略...
    }
    reader.endArray();
  }

同樣的原理bitmap的width應(yīng)該就是reader.selectName(_assetsNames) == 2的場(chǎng)景,
height應(yīng)該就是reader.selectName(_assetsNames) == 3的場(chǎng)景逛尚。

所以最終得出的結(jié)論是assets里面的w會(huì)作為bitmap的寬绰寞,h會(huì)作為bitmap的高即最終值為120*120滤钱。
那我們?cè)倩氐嚼L制的那兩句代碼
ImageLayer#drawLayer方法:

    var src =
        Rect.fromLTWH(0, 0, bitmap.width.toDouble(), bitmap.height.toDouble());
    var dst = Rect.fromLTWH(
        0, 0, bitmap.width * density, bitmap.height.toDouble() * density);
    canvas.drawImageRect(bitmap, src, dst, paint);

drawImageRect方法可以把圖片上的一個(gè)矩形部分铜靶,以填充至滿的形式繪制到另一個(gè)矩形中他炊。
而這里dst的寬高是120density(density即為像素密度)
到這里我猜測(cè)應(yīng)該是圖片尺寸大于了120
120,所以在繪制的時(shí)候src只截圖了一部分矩形,然后填沖到了一個(gè)120 * density的正方形中痊末。這樣就放大了點(diǎn)贊的圖片蚕苇。我查看了一下圖片的尺寸:

like-fill.png

果然尺寸設(shè)置成了240*240凿叠,難怪會(huì)在動(dòng)畫顯示的時(shí)候放大了兩倍。


總結(jié)

總的來(lái)說(shuō)問(wèn)題解決起來(lái)不難幔嫂,將圖片縮小到120*120的尺寸即可辆它。
注意事項(xiàng):經(jīng)過(guò)這次的踩坑問(wèn)題可以知道履恩,以后在使用Flutter的Lottie組件進(jìn)行顯示lottie動(dòng)畫的時(shí)候,如果json文件里面有設(shè)置image的圖片呢蔫,那么對(duì)應(yīng)的圖片的寬高必須要跟json文件里面設(shè)置的image圖片的w和h參數(shù)保持一致切心,否則顯示出來(lái)的圖片就會(huì)放大或者縮小。

ps: 總算是解決了問(wèn)題绽昏,并且搞清楚了原因。這個(gè)地方花費(fèi)了差不過(guò)一天的時(shí)間來(lái)搞明白全谤。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末认然,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子腾务,更是在濱河造成了極大的恐慌,老刑警劉巖窿撬,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異叙凡,居然都是意外死亡尤仍,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門狭姨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)宰啦,“玉大人,你說(shuō)我怎么就攤上這事饼拍∩哪#” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵师抄,是天一觀的道長(zhǎng)漓柑。 經(jīng)常有香客問(wèn)我,道長(zhǎng)叨吮,這世上最難降的妖魔是什么辆布? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮茶鉴,結(jié)果婚禮上锋玲,老公的妹妹穿的比我還像新娘。我一直安慰自己涵叮,他們只是感情好惭蹂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著割粮,像睡著了一般盾碗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上舀瓢,一...
    開(kāi)封第一講書(shū)人閱讀 51,754評(píng)論 1 307
  • 那天廷雅,我揣著相機(jī)與錄音,去河邊找鬼京髓。 笑死航缀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的朵锣。 我是一名探鬼主播谬盐,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼诚些!你這毒婦竟也來(lái)了飞傀?” 一聲冷哼從身側(cè)響起皇型,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎砸烦,沒(méi)想到半個(gè)月后弃鸦,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡幢痘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年唬格,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颜说。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡购岗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出门粪,到底是詐尸還是另有隱情喊积,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布玄妈,位于F島的核電站乾吻,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏拟蜻。R本人自食惡果不足惜绎签,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酝锅。 院中可真熱鬧诡必,春花似錦、人聲如沸屈张。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)阁谆。三九已至,卻和暖如春愉老,著一層夾襖步出監(jiān)牢的瞬間场绿,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工嫉入, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留焰盗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓咒林,卻偏偏與公主長(zhǎng)得像熬拒,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子垫竞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容