Flutter_cached_network_image: Hero animation doesn't perform as expected first time

Created on 13 Dec 2019  路  15Comments  路  Source: Baseflow/flutter_cached_network_image

I'm transitioning from

Hero(
  tag: imageModel.url,
  child: CachedNetworkImage(
    imageUrl: imageModel.url,
    fit: BoxFit.cover,
  ),
)

to

final item = widget.galleryItems[index];
PhotoViewGalleryPageOptions(
  imageProvider: CachedNetworkImageProvider(
     item.url,
  ),
  initialScale: PhotoViewComputedScale.contained,
  minScale: PhotoViewComputedScale.contained,
  maxScale: PhotoViewComputedScale.covered * 1.2,
  heroAttributes: PhotoViewHeroAttributes(tag: item.url),
);

The first transition doesn't use hero animation. When I navigate back it starts working. All the subsequent transitions use hero animation.

The issue doesn't happen when I use plain NetworkImage/Image.network provider/widget

triage

Most helpful comment

ok so this is more to do with how photo_view implements their Hero, than this package. From their docs:

To use within an hero animation, specify heroAttributes. When heroAttributes is specified, the image provider retrieval process should be sync. (source)

In their implementation, if the image is not completely loaded by the time the first call to build then the Hero is never actually put into the PhotoView, and so the Hero animation cannot happen. There are several related issues regarding this limitation:

https://github.com/renancaraujo/photo_view/issues/191
https://github.com/renancaraujo/photo_view/issues/128

However, in testing I managed to find a workaround. Instead of using a CachedNetworkImage widget and transitioning to a PhotoView, use a regular Image with a CachedNetworkImageProvider. The Hero transition works in the following example:

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'CachedNetworkImage Hero',
        theme: ThemeData(primarySwatch: Colors.blue),
        home: MyHomePage(),
      );
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('CachedNetworkImage')),
      body: _gridView(),
    );
  }

  _gridView() {
    return GridView.builder(
        itemCount: 250,
        gridDelegate:
            const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
        itemBuilder: (BuildContext context, int index) {
          final url = 'https://loremflickr.com/500/500/music?lock=$index';

          return InkWell(
            onTap: () => Navigator.of(context).push(
              MaterialPageRoute(
                builder: (BuildContext context) => Scaffold(
                  appBar: AppBar(title: Text('Detail')),
                  body: PhotoView(
                    imageProvider: CachedNetworkImageProvider(url),
                    heroAttributes: PhotoViewHeroAttributes(
                      tag: url,
                    ),
                  ),
                ),
              ),
            ),
            child: Hero(
              tag: url,
              child: Image(
                image: CachedNetworkImageProvider(
                  url,
                ),
              ),
            ),
          );
        });
  }
}

So I guess the next question is: why does CachedNetworkImageProvider work differently to CachedNetworkImage?

All 15 comments

Video demonstrating the issue:

2(1)

If you add a placeholder to your CachedNetwork image does it work?

No, still same issue. Tried this:

Hero(
tag: imageModel.url,
child: CachedNetworkImage(
  placeholder: (context, _) => Container(
    color: Colors.red,
  ),
  imageUrl: imageModel.url,
  fit: BoxFit.cover,
  ),
),

Being the same thing here.

Here's a minimal example showing how to use Hero with CachedNetworkImage. I have not been able to replicate the problem you are experiencing:

https://gist.github.com/MichaelMarner/52c18de80c8813628001cc725beefb0b

Are you able to modify the example and replicate the problem?

The modified Gist with error reproducible is below. It's using photo_view library.

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'CachedNetworkImage Hero',
        theme: ThemeData(primarySwatch: Colors.blue),
        home: MyHomePage(),
      );
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('CachedNetworkImage')),
      body: _gridView(),
    );
  }

  _gridView() {
    return GridView.builder(
        itemCount: 250,
        gridDelegate:
            const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
        itemBuilder: (BuildContext context, int index) {
          final url = 'https://loremflickr.com/500/500/music?lock=$index';

          return InkWell(
            onTap: () => Navigator.of(context).push(
              MaterialPageRoute(
                builder: (BuildContext context) => Scaffold(
                  appBar: AppBar(title: Text('Detail')),
                  body: PhotoViewGallery.builder(
                    scrollPhysics: const ClampingScrollPhysics(),
                    builder: _buildItem,
                    itemCount: 250,
                  ),
                ),
              ),
            ),
            child: Hero(
              tag: url,
              child: CachedNetworkImage(
                imageUrl: url,
                placeholder: _loader,
                errorWidget: _error,
              ),
            ),
          );
        });
  }

  PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) {
    final url = 'https://loremflickr.com/500/500/music?lock=$index';
    return PhotoViewGalleryPageOptions(
      // imageProvider: NetworkImage(
      //   url,
      // ),
      imageProvider: CachedNetworkImageProvider(
        url,
      ),
      initialScale: PhotoViewComputedScale.contained,
      minScale: PhotoViewComputedScale.contained,
      maxScale: PhotoViewComputedScale.covered * 1.2,
      heroAttributes: PhotoViewHeroAttributes(tag: url),
    );
  }

  Widget _loader(BuildContext context, String url) => Center(
        child: CircularProgressIndicator(),
      );

  Widget _error(BuildContext context, String url, dynamic error) {
    return Center(child: const Icon(Icons.error));
  }
}

ok so this is more to do with how photo_view implements their Hero, than this package. From their docs:

To use within an hero animation, specify heroAttributes. When heroAttributes is specified, the image provider retrieval process should be sync. (source)

In their implementation, if the image is not completely loaded by the time the first call to build then the Hero is never actually put into the PhotoView, and so the Hero animation cannot happen. There are several related issues regarding this limitation:

https://github.com/renancaraujo/photo_view/issues/191
https://github.com/renancaraujo/photo_view/issues/128

However, in testing I managed to find a workaround. Instead of using a CachedNetworkImage widget and transitioning to a PhotoView, use a regular Image with a CachedNetworkImageProvider. The Hero transition works in the following example:

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => MaterialApp(
        title: 'CachedNetworkImage Hero',
        theme: ThemeData(primarySwatch: Colors.blue),
        home: MyHomePage(),
      );
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('CachedNetworkImage')),
      body: _gridView(),
    );
  }

  _gridView() {
    return GridView.builder(
        itemCount: 250,
        gridDelegate:
            const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
        itemBuilder: (BuildContext context, int index) {
          final url = 'https://loremflickr.com/500/500/music?lock=$index';

          return InkWell(
            onTap: () => Navigator.of(context).push(
              MaterialPageRoute(
                builder: (BuildContext context) => Scaffold(
                  appBar: AppBar(title: Text('Detail')),
                  body: PhotoView(
                    imageProvider: CachedNetworkImageProvider(url),
                    heroAttributes: PhotoViewHeroAttributes(
                      tag: url,
                    ),
                  ),
                ),
              ),
            ),
            child: Hero(
              tag: url,
              child: Image(
                image: CachedNetworkImageProvider(
                  url,
                ),
              ),
            ),
          );
        });
  }
}

So I guess the next question is: why does CachedNetworkImageProvider work differently to CachedNetworkImage?

Any updates on this issue? I am using a thumbnail of a photo. When clicked, the real photo shows and I would like to use the thumbnail as a placeholder (and use a hero for that) until the real photo is fully loaded.

Just to elaborate on what I am trying to achieve (which is not working)

Hero(
              tag: "myTag",
              child: CachedNetworkImage(
                  imageUrl: widget.imageURL,
                  placeholder: (context, url) =>
                      Image.network(widget.thumbnail),
                  errorWidget: (context, url, error) => Icon(Icons.error)),
            ),

and on the calling end:

Hero(
                tag: "myTag",
                child: CachedNetworkImage(
                    imageUrl: widget.thumbnail,
                    placeholder: (context, url) => CircularProgressIndicator(),
                    errorWidget: (context, url, error) => Icon(Icons.error)),
              )

@Navil This is most likely a different issue - As you are using Image.network for your placeholder, Flutter is going to redownload the thumbnail.

Try using another CachedNetworkImage as your placeholder:

Hero(
              tag: "myTag",
              child: CachedNetworkImage(
                  imageUrl: widget.imageURL,
                  placeholder: (context, url) =>
                      CachedNetworkImage(imageUrl: widget.thumbnail),
                  errorWidget: (context, url, error) => Icon(Icons.error)),
            ),

(note, untested)

My code block is working. Not depent PhotoView package.

              return ClipRRect(
                borderRadius: BorderRadius.all(Radius.circular(6.0)),
                child: InkWell(
                    borderRadius: BorderRadius.all(Radius.circular(10)),
                    onTap: () {
                      Navigator.push(context, MaterialPageRoute(builder: (_) {
                        return PhotoFullScreen(
                            medya.fileOwnerUrl, medya.fileOwnerUrl.toString());
                      }));
                    },
                    child: Container(
                        width: 180,
                        color: Colors.black12,
                        child: Hero(
                          tag: medya
                              .fileOwnerUrl,
                          child: CachedNetworkImage(
                            imageUrl: medya.fileOwnerUrl,
                            placeholder: (context, url) =>
                                CircularProgressIndicator(),
                            errorWidget: (context, url, error) =>
                                Icon(Icons.error),
                            fit: BoxFit.cover,
                          ),
                        ))),
              );
class PhotoFullScreen extends StatelessWidget {
  final String photoURL;
  final String heroTag;

  const PhotoFullScreen(this.photoURL, this.heroTag, {Key key})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        child: Center(
          child: Hero(
            tag: heroTag,
            child: CachedNetworkImage(
              imageUrl: photoURL,
              placeholder: (context, url) => CircularProgressIndicator(),
              errorWidget: (context, url, error) => Icon(Icons.error),
              fit: BoxFit.cover,
            ),
          ),
        ),
        onTap: () {
          Navigator.pop(context);
        },
        onVerticalDragEnd: (s) {
          Navigator.pop(context);
        },
      ),
    );
  }
}

video demonstrating

hero-animations

The following approach is a feasible solution, the animation and image load transition both work properly.

Hero(
   tag: url,
   //Apply [FadeInImage] to provide loading transition
   child: FadeInImage(
   // Use [CachedNetworkImageProvider] to active the first animation
   image: CachedNetworkImageProvider(url),
   placeholder: AssetImage('PATH'),
   ),
),
PhotoView(
   imageProvider: CachedNetworkImageProvider(url),
   heroAttributes: PhotoViewHeroAttributes(
   tag: url,
   ),
),

In 2.3.0-beta the widget uses the CachedNetworkImageProvider, so the issue with the hero should be fixed

I can't get the Hero animation to work with CachedNetworkImage (2.3.3) and PhotoView (0.10.3).

The code I use:

InkWell(
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) =>
                Scaffold(
                  backgroundColor: Colors.black,
                  appBar: AppBar(backgroundColor: Colors.transparent),
                  body: PhotoView(
                    imageProvider: CachedNetworkImageProvider(widget.coverImageUrl),
                    initialScale: PhotoViewComputedScale.contained,
                    minScale: PhotoViewComputedScale.contained,
                    maxScale: PhotoViewComputedScale.covered * 1.8,
                    heroAttributes: const PhotoViewHeroAttributes(
                      tag: 'eventCoverImage',
                      transitionOnUserGestures: true,
                    ),
                  ),
                ),
          ),
        );
      },
      child: Hero(
        tag: 'eventCoverImage',
        transitionOnUserGestures: true,
        child: CachedNetworkImage(
          imageUrl: imageUrl,
          height: height,
          fit: fit,
        ),
      ),
    )

What if you just wrap a Hero around PhotoView? I don't know how PhotoView's heroAttributes work.

It does not work.
I didn't dig into PhotoView code yet, as the Hero animation is nice-to-have in my case.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

sososdk picture sososdk  路  5Comments

BerndWessels picture BerndWessels  路  6Comments

flutteradv picture flutteradv  路  6Comments

creativecreatorormaybenot picture creativecreatorormaybenot  路  4Comments

chow2324 picture chow2324  路  4Comments