踩坑之路:Flutter Lottie動畫組件踩坑

背景

Flutter中我使用的是這個Lottie組件
我在做一個點贊的動畫的時候,就是很簡單的將like.json放到assets目錄下,然后再同一個目錄下面創了了一個images文件夾,文件夾下面放著like-fill.png圖片
注: 這么做的原因是json文件有下面這段代碼:

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

然后使用下面這段代碼加載lottie動畫:

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

最終顯示結果如下方gif一樣:


異常點贊.gif

而正常的結果應該是跟下面這個一樣


正常點贊.gif

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

探究

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


Lottie時序圖1.jpg
  1. 創建LottieBuilder對象,
  2. 通過LottieBuilder對象創建AssetsLottie對象,主要根據生成的key值從cache中獲取已經創建的Future<LottieComposition>
  3. 如果不存在那么先創建一個Future<LottieComposition>,然后加載Assets文件夾下對應的Lottie文件
  4. 然后在FutureBuilder的build方法通過future獲取LottieComposition對象,構建Lottie的Widget


    Lottie時序圖2.jpg
  5. Lottie 的Widget創建RawLottie,然后創建RenderLottie,創建LottieDrawable, 創建CompositionLayer
  6. CompositionLayer根據傳入的Models創建不同的BaseLayer對象(點贊的圖是一個Image類型,所以創建的是ImageLayer)
  7. 由于RenderLottie是個RenderBox對象,會執行paint方法所以最終會執行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();
  }

很顯然從最終繪制的代碼看,是調用了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;
    }
  }

從上述代碼看,應該是從Composition里面獲取的images數組。Composition在哪里設置的呢?其實第一張時序圖里面已經給出了地方(就是調用的LottieComposition的fromBytes方法獲取的)我們再來看下這個方法的主要邏輯:

  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;
  }

發現是調用了LottieCompositionParser的parse方法獲取的,那么可想而知里面的操作應該就是類似乎xml解析一樣的根據各種tag獲取對應的值。

  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;
  }

我們那個點贊的圖片like-fill.png其實是在assets的字段中,也就是reader.selectName(_names) == 7的場景。所以會執行_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應該就是reader.selectName(_assetsNames) == 2的場景,
height應該就是reader.selectName(_assetsNames) == 3的場景。

所以最終得出的結論是assets里面的w會作為bitmap的寬,h會作為bitmap的高即最終值為120*120。
那我們再回到繪制的那兩句代碼
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方法可以把圖片上的一個矩形部分,以填充至滿的形式繪制到另一個矩形中。
而這里dst的寬高是120density(density即為像素密度)
到這里我猜測應該是圖片尺寸大于了120
120,所以在繪制的時候src只截圖了一部分矩形,然后填沖到了一個120 * density的正方形中。這樣就放大了點贊的圖片。我查看了一下圖片的尺寸:

like-fill.png

果然尺寸設置成了240*240,難怪會在動畫顯示的時候放大了兩倍。


總結

總的來說問題解決起來不難,將圖片縮小到120*120的尺寸即可。
注意事項:經過這次的踩坑問題可以知道,以后在使用Flutter的Lottie組件進行顯示lottie動畫的時候,如果json文件里面有設置image的圖片,那么對應的圖片的寬高必須要跟json文件里面設置的image圖片的w和h參數保持一致,否則顯示出來的圖片就會放大或者縮小。

ps: 總算是解決了問題,并且搞清楚了原因。這個地方花費了差不過一天的時間來搞明白。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容