2d-inc / Flare-Flutter

Load and get full control of your Rive files in a Flutter project using this library.
https://rive.app/
MIT License
2.55k stars 469 forks source link

FlareActor flashing when used with Flutter Hero widget #111

Closed DamienMrtl closed 5 years ago

DamienMrtl commented 5 years ago

When using a FlareActor inside a Flutter Hero (to transition the Actor to a new page for example), we see two "flashes". One when the Hero creates a new Actor to make the transition to destination page and once again when creating the Actor on the destination page at the end of the transition.

Video of the issue

It looks like this is due to the loading time when displaying an Actor dynamically.

Tested with Flutter v1.5.4-hotfix.2 & Flare-Flutter 1.5.2 in debug and release modes

Code to replicate (main.dart):

import 'package:flutter/material.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:flutter/scheduler.dart' show timeDilation;

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    timeDilation = 5;
    return Container(
      color: Colors.white,
      child: MaterialApp(
        title: "Flare Bot",
        routes: {
          "/page1": (context) => Page1(),
          "/page2": (context) => Page2(),
        },
        home: Page1(),
      ),
    );
  }
}

class Page1 extends StatelessWidget {
  const Page1({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => Navigator.pushNamed(context, "/page2"),
      child: Container(
        child: Align(
            alignment: Alignment.topCenter,
            child: Hero(
              tag: "tag",
              child: Container(
                width: 300,
                height: 300,
                child: Stack(
                  children: <Widget>[
                    FlareActor(
                      'assets/Chatbot-back.flr',
                    ),
                    FlareActor(
                      'assets/Chatbot-front.flr',
                    )
                  ],
                ),
              ),
            )),
      ),
    );
  }
}

class Page2 extends StatelessWidget {
  const Page2({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => Navigator.pushNamed(context, "/page1"),
      child: Align(
          alignment: Alignment.bottomCenter,
          child: Container(
            width: 300,
            height: 300,
            child: Hero(
              tag: "tag",
              child: Stack(
                children: <Widget>[
                  FlareActor(
                    'assets/Chatbot-back.flr',
                    fit: BoxFit.fitHeight,
                    snapToEnd: false,
                    shouldClip: true,
                  ),
                  FlareActor(
                    'assets/Chatbot-front.flr',
                    animation: "face-to-broken",
                    fit: BoxFit.fitHeight,
                    snapToEnd: false,
                    shouldClip: true,
                  )
                ],
              ),
            ),
          )),
    );
  }
}

And using the 2 assets : assets.zip

Do you know any workaround or tricks to display the Actor faster, maybe using some sort of caching ?

luigi-rosso commented 5 years ago

I think the right way to do this is to load the Flare file in the context of the widget that will host the hero. Then you'd need to use a custom LeafRenderWidget (similar to the FlareActor) which displays the Flare file and shares it across the different LeafRenderWidgets.

Flare does use caching internally (and I actually tested your code with a pre-warmed cache with the code below) but the flicker is still there as retrieving the items from cache is also asynchronous:

Prewarmed (still flickers) cache:

import 'package:flare_flutter/flare_cache.dart';
import 'package:flare_flutter/flare_cache_asset.dart';
import 'package:flutter/services.dart';

// ... leaving out other imports

Future<void> main() async
{
    FlareCacheAsset asset1 = await cachedActor(rootBundle, 'assets/Chatbot-back.flr');
    asset1.ref();
    FlareCacheAsset asset2 = await cachedActor(rootBundle, 'assets/Chatbot-front.flr');
    asset2.ref();

    runApp(MyApp());
}

The right solution is a more involved example, I'll provide you with that as soon as I have a little time to spend on it.

luigi-rosso commented 5 years ago

Alright give this example a try: https://github.com/luigi-rosso/flare_flutter_shared_artboard

The two pages share the same Flare artboards and controller for the animation so that they can control the same Flare artboard without reloading and across different widgets. Result is no flickering and better animation control!

Notice how the animation gets triggered when the user taps: https://github.com/luigi-rosso/flare_flutter_shared_artboard/blob/b08a67539728efbaf9814158315ad6d443c56c26/lib/main.dart#L70-L73

DamienMrtl commented 5 years ago

Wow thanks for your reply @luigi-rosso . I wasn't asking for so much, now even the animation is working correctly while the Flare is moving 😃 . Thank you so much.

BTW: I first had a "Type" error when building your example. To make it work I had to cast _indices to Int32List on line 1025 of "flare.dart" maybe because I use Flutter 1.5.4-hotfix.2

 _canvasVertices = ui.Vertices.raw(ui.VertexMode.triangles, _vertexBuffer,
        indices: _indices as Int32List, textureCoordinates: _uvBuffer);
luigi-rosso commented 5 years ago

Oh yes, that's related to breaking changes between dev/master and stable (Flutter 1.5.4-hotfix.2). I'd strongly recommend you use the dev/master Flutter channel as there are other fixes in there that are worth having (like aot compiler bugs that will manifest as invisible shapes on certain device architectures with Flare) and also faster loading as we can perform the whole Flare file load in an isolate.

JHBitencourt commented 4 years ago

For those who are trying to implement the same thing and are wondering how to use the new cachedActor() method:

Old way:

final asset = await cachedActor(rootBundle, 'assets/file.flr');
asset.ref();

New way:

final provider = AssetFlare(bundle: rootBundle, name: 'assets/file.flr');
final asset = await cachedActor(provider);
asset.ref();