Hero animation with gallery flicker Flutter

Viewed 273

I'm trying to create a basic Hero animation between a gallery grid view page (using photo manager plugin) and a detail page. When the hero animation is done both back and forward, the picture is flickering .

Here is the example I've made :

enter image description here

The full code runnable :

import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:photo_manager/photo_manager.dart';

void main() {
  runApp(const TestApp());
}

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

  @override
  State<TestApp> createState() => _TestAppState();
}

class _TestAppState extends State<TestApp> {
  final List<AssetEntity> assetsList = [];

  bool granted = false;

  void loadAssets() async {
    granted = await PhotoManager.requestPermission();
    if (granted) {
      FilterOptionGroup option = FilterOptionGroup()
        ..addOrderOption(const OrderOption(
          type: OrderOptionType.createDate,
          asc: false,
        ));

      final albums = await PhotoManager.getAssetPathList(filterOption: option, type: RequestType.image);

      print("albums : $albums");
      if (albums.isNotEmpty) {
        var alb = albums.where((element) {
          return element.name == 'Test';
        });
        var album = alb.first;
        // Now that we got the album, fetch all the assets it contains
        List<AssetEntity> currentList =
            await album.getAssetListRange(start: 0, end: 200);
        // Update the state and notify UI
        assetsList.clear();
        assetsList.addAll(currentList);
      }
      setState(() {});
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
          body: granted
              ? _gridView()
              : Center(
                  child: Container(
                      color: Colors.blue,
                      width: 200,
                      height: 200,
                      child: TextButton(
                        onPressed: () async {
                          granted = await PhotoManager.requestPermission();
                          setState(() {});
                        },
                        child: const Text(
                          "Ask permission",
                          style: TextStyle(color: Colors.white),
                        ),
                      )),
                )),
    );
  }

  Widget _gridView() {
    return GridView.builder(
        itemCount: assetsList.length,
        gridDelegate:
            const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
        itemBuilder: (context, index) {
          return Hero(
              createRectTween: (Rect? begin, Rect? end) {
                RectTween _rectTween = RectTween(begin: begin, end: end);
                return _rectTween;
              },
              tag: assetsList[index].id,
              child: GalleryThumbnail(
                asset: assetsList[index],
                onTap: (bytes) {
                  Navigator.push(context, MaterialPageRoute(builder: (context) {
                    return DetailsPage(asset: assetsList[index], bytes: bytes);
                  }));
                },
              ));
        });
  }
}

class GalleryThumbnail extends StatelessWidget {
  final AssetEntity asset;
  final ValueChanged<Uint8List> onTap;

  const GalleryThumbnail({Key? key, required this.asset, required this.onTap})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<Uint8List?>(
      future: Platform.isIOS
          ? asset.thumbDataWithOption(
              ThumbOption.ios(
                  width: 500,
                  height: 500,
                  deliveryMode: DeliveryMode.opportunistic,
                  resizeMode: ResizeMode.fast,
                  resizeContentMode: ResizeContentMode.fit,
                  quality: 100
                  // resizeContentMode: ResizeContentMode.fill,
                  ),
            )
          : asset.thumbDataWithSize(250, 250),
      builder: (_, snapshot) {
        final bytes = snapshot.data;
        if (snapshot.hasError) {
          return Container();
        }
        // If we have no data
        if (bytes == null) return Container();
        // If there's data, display it as an image
        return GestureDetector(
            onTap: () {
              onTap(bytes);
            },
            child: Image.memory(bytes, fit: BoxFit.cover,gaplessPlayback: true,));
      },
    );
  }
}

class DetailsPage extends StatefulWidget {
  final AssetEntity asset;
  final Uint8List bytes;

  const DetailsPage({Key? key, required this.asset, required this.bytes})
      : super(key: key);

  @override
  _DetailsPageState createState() => _DetailsPageState();
}

class _DetailsPageState extends State<DetailsPage> {
  late ImageProvider _imageProvider;

  Future<void> loadFile() async {
    try {
      File? file = await widget.asset.file;
      if (file == null) return;

      _imageProvider = Image.file(file).image;
      setState(() {});
    } catch (e) {
      print("error to load file : " + e.toString());
    }
  }

  @override
  void initState() {
    _imageProvider = Image.memory(widget.bytes).image;
    loadFile();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    double ratio = widget.asset.height / widget.asset.width;
    return Scaffold(
      body: Stack(
        children: [
          Hero(
            createRectTween: (Rect? begin, Rect? end) {
              RectTween _rectTween = RectTween(begin: begin, end: end);
              return _rectTween;
            },
            tag: widget.asset.id,
            child: Center(
              child: Image(image: _imageProvider,
                  width: MediaQuery.of(context).size.width,
                  height: MediaQuery.of(context).size.height*ratio,
                  gaplessPlayback: true),
            ),
          ),
          const SafeArea(
              child: Padding(
                padding: EdgeInsets.all(8.0),
                child: BackButton(),
              )),
        ],
      ),
    );
  }
}

I've implemented hero animation somewhere else in my project and I'm not getting this behavior where as here I do.

Why this is happening and how to correct this ?

2 Answers

Add

transitionOnUserGestures: true,

to Hero() widget in current & next page

This happens because the Hero of the image thumbail wraps the FutureBuilder, and then the future triggers again during the animation, wraps the Image instead of the FutureBuilder.

Related