dnfield / lottie-flutter

A pure Dart/Flutter implementation of Lottie
Apache License 2.0
115 stars 24 forks source link

Google IO Android Lottie Animations - Bad state: No element #28

Open workerbee22 opened 5 years ago

workerbee22 commented 5 years ago

Using the lottie_flutter package version 0.2.0 and some quite 'famous' Google IO 2018 app lottie animation files. Namely the count down digits here:

Google IO 2018 Open source lottie animation files

io18_logo.json seems fine, but the digits like 0.json throw:

The following StateError was thrown during paint(): Bad state: No element

import 'dart:async';
import 'dart:convert';
import 'dart:ui';
import 'package:lottie_flutter/lottie_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;

const List<String> assetNames = const <String>[
  'assets/Indicators2.json',
  'assets/happy_gift.json',
  'assets/empty_box.json',
  'assets/muzli.json',
  'assets/hamburger_arrow.json',
  'assets/motorcycle.json',
  'assets/emoji_shock.json',
  'assets/checked_done_.json',
  'assets/favourite_app_icon.json',
  'assets/preloader.json',
  'assets/walkthrough.json',
  // File missing from repo code
  // 'assets/rrect.json',
  'assets/0.json',
  'assets/1.json',
  'assets/2.json',
  'assets/3.json',
  'assets/io18_logo.json',
];

void main() {
  runApp(new DemoApp());
}

class DemoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Lottie Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const LottieDemo(),
    );
  }
}

class LottieDemo extends StatefulWidget {
  const LottieDemo({Key key}) : super(key: key);

  @override
  _LottieDemoState createState() => new _LottieDemoState();
}

class _LottieDemoState extends State<LottieDemo>
    with SingleTickerProviderStateMixin {
  LottieComposition _composition;
  String _assetName;
  AnimationController _controller;
  bool _repeat;

  @override
  void initState() {
    super.initState();

    _repeat = false;
    _loadButtonPressed(assetNames.last);
    _controller = new AnimationController(
      duration: const Duration(milliseconds: 1),
      vsync: this,
    );
    _controller.addListener(() => setState(() {}));
  }

  void _loadButtonPressed(String assetName) {
    loadAsset(assetName).then((LottieComposition composition) {
      setState(() {
        _assetName = assetName;
        print('*** Asset selected: ' + assetName);
        _composition = composition;
        _controller.reset();
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: const Text('Lottie Demo'),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            new DropdownButton<String>(
              items: assetNames
                  .map((String assetName) => new DropdownMenuItem<String>(
                child: new Text(assetName),
                value: assetName,
              ))
                  .toList(),
              hint: const Text('Choose an asset'),
              value: _assetName,
              onChanged: (String val) => _loadButtonPressed(val),
            ),
            new Text(_composition?.bounds?.size?.toString() ?? ''),
            new Lottie(
              composition: _composition,
              size: const Size(300.0, 300.0),
              controller: _controller,
            ),
            new Slider(
              value: _controller.value,
              onChanged: _composition != null
                  ? (double val) => setState(() => _controller.value = val)
                  : null,
            ),
            new Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  new IconButton(
                    icon: const Icon(Icons.repeat),
                    color: _repeat ? Colors.black : Colors.black45,
                    onPressed: () => setState(() {
                      _repeat = !_repeat;
                      if (_controller.isAnimating) {
                        if (_repeat) {
//                          _controller.forward().then<Null>(
//                                  (Null nul) => _controller.repeat());
                          // TODO Line above is ERROR from the repo code, so just comment out for now
                          _controller.repeat();
                        } else {
                          _controller.forward();
                        }
                      }
                    }),
                  ),
                  new IconButton(
                    icon: const Icon(Icons.fast_rewind),
                    onPressed: _controller.value > 0 && _composition != null
                        ? () => setState(() => _controller.reset())
                        : null,
                  ),
                  new IconButton(
                    icon: _controller.isAnimating
                        ? const Icon(Icons.pause)
                        : const Icon(Icons.play_arrow),
                    onPressed: _controller.isCompleted || _composition == null
                        ? null
                        : () {
                      setState(() {
                        if (_controller.isAnimating) {
                          _controller.stop();
                        } else {
                          if (_repeat) {
                            _controller.repeat();
                          } else {
                            _controller.forward();
                          }
                        }
                      });
                    },
                  ),
                  new IconButton(
                    icon: const Icon(Icons.stop),
                    onPressed: _controller.isAnimating && _composition != null
                        ? () {
                      _controller.reset();
                    }
                        : null,
                  ),
                ]),
          ],
        ),
      ),
    );
  }
}

Future<LottieComposition> loadAsset(String assetName) async {
  return await rootBundle
      .loadString(assetName)
      .then<Map<String, dynamic>>((String data) => json.decode(data))
      .then((Map<String, dynamic> map) => new LottieComposition.fromMap(map));
}

Flutter Doctor: [✓] Flutter (Channel stable, v1.2.1, on Mac OS X 10.14.4 18E226, locale en-AU) • Flutter version 1.2.1 at /Users/gamma/Documents/flutter • Framework revision 8661d8aecd (9 weeks ago), 2019-02-14 19:19:53 -0800 • Engine revision 3757390fa4 • Dart version 2.1.2 (build 2.1.2-dev.0.0 0a7dcf17eb)

[✓] Android toolchain - develop for Android devices (Android SDK version 28.0.3) • Android SDK at /Users/gamma/Library/Android/sdk • Android NDK location not configured (optional; useful for native profiling support) • Platform android-28, build-tools 28.0.3 • ANDROID_HOME = /Users/gamma/Library/Android/sdk • Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1343-b01) • All Android licenses accepted.

[✓] iOS toolchain - develop for iOS devices (Xcode 10.2) • Xcode at /Applications/Xcode.app/Contents/Developer • Xcode 10.2, Build version 10E125 • ios-deploy 1.9.4 • CocoaPods version 1.5.3

[✓] Android Studio (version 3.4) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin version 34.0.2 • Dart plugin version 183.5901 • Java version OpenJDK Runtime Environment (build 1.8.0_152-release-1343-b01)

[✓] VS Code (version 1.33.1) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 2.25.1

[✓] Connected device (1 available) • Nexus 6P • CVH7N15A17000241 • android-arm64 • Android 8.1.0 (API 27)

• No issues found!

dnfield commented 5 years ago

Ahhh. This throws a bad exception because of using first without checking if the iterator has an element. But it's also somehow mutating the path and losing it.

dnfield commented 5 years ago

Similar comment as before - this is likely fixable, but right now I'd prefer to focus my Lottie related efforts on the rewrite rather than fixing this code, which really needs a bit of work.

workerbee22 commented 5 years ago

Sounds like then rewrite first, but these might be good test files given the high profile of the Google IO app and making sure the package is robust enough to handle most Lottie files. Any suggestions in the meantime? like any way I might review or 'clean' the actual .json files to work with the current 0.2.0 package version?

dnfield commented 5 years ago

The bug listed here is related to trim paths - if the file has those it seems like it may have issues - although there are definitely some files that work. Unfortunately you probably won't do better than just inspecting the animation in the sample app right now.

workerbee22 commented 5 years ago

Ok diving into this a little further and after checking Lottie json doco for Trim here:

AirBnB Lottie Trim doco

If I take one of the example Google IO files like 2.json and modify it to create a 2modified.json and based on the doco remove the 2 offending objects starting with "ty": "tm" from the json file, I can then use the 2modified.json without any issues. Both of these files attached for reference.

2.json.txt 2modified.json.txt

I also confirmed that io18_logo.json seems fine because it has no such "ty": "tm" objects in the json file ie. no trims.

But now the issue is that removing those trim objects from the json file fundamentally changes the look of the animation. But it might help you find a fix.