Re-usable 带有资产图像或 FadeInImage 网络图像的英雄小部件

Re-usable Hero widget with image from asset or FadeInImage network image

我正在尝试创建一个 re-usable 英雄小部件,其中可能会在不同的上下文中使用资产图像或网络图像。目前,在我使用网络图像的地方,它按预期工作,例如将自定义小部件作为 ListView 项目中的 Leading,在选择该 ListView 项目时动画化为 AppBar 的标题,以及在 pop-ing 上 return 到 ListView 的视图。

但是,在我将本地资产与我的自定义 AscHero 小部件结合使用的情况下,英雄动画不会出现。

(我对 Flutter/Dart 和 OOP 仍然很陌生,所以请随时指出我在做什么 sub-optimal 或愚蠢的事情:)

定制小部件:

import 'package:flutter/material.dart';
import 'package:transparent_image/transparent_image.dart';

class AscHero extends StatelessWidget {
  final String thumbUrl;
  final AssetImage assetImage;
  final Object tag;
  final String title;
  final double radius;

  const AscHero({
    Key? key,
    required this.tag,
    required this.title,
    this.thumbUrl = '',
    this.assetImage = const AssetImage(''),
    this.radius = 48,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final bool haveUrlOrAssetImg =
        (thumbUrl != '' || assetImage != const AssetImage(''));
    final bool haveUrl = (thumbUrl != '');
    //assert(thumbUrl != '' && assetImage != const AssetImage(''));
    return SizedBox(
      child: ClipOval(
        child: Material(
          color: Colors.lightBlue.withOpacity(0.25),
          child: Center(
            child: (haveUrlOrAssetImg)
                ? Hero(
                    tag: tag,
                    child: (haveUrl)
                        ? FadeInImage.memoryNetwork(
                            placeholder: kTransparentImage,
                            image: thumbUrl,
                            fit: BoxFit.cover,
                            width: radius,
                            height: radius,
                          )
                        : Image(image: assetImage),
                    flightShuttleBuilder: (
                      BuildContext flightContext,
                      Animation<double> animation,
                      HeroFlightDirection flightDirection,
                      BuildContext fromHeroContext,
                      BuildContext toHeroContext,
                    ) {
                      return Container(
                        decoration: BoxDecoration(
                          shape: BoxShape.circle,
                          image: DecorationImage(
                            image: NetworkImage(thumbUrl),
                          ),
                        ),
                      );
                    },
                  )
                : Text('${tag.toString()} $title'),
          ),
        ),
      ),
      width: radius,
      height: radius,
    );
  }
}

什么有效:

import 'package:flutter/material.dart';
import 'package:transparent_image/transparent_image.dart';
import 'package:agent_01/stocks/stocks.dart';

class StockListItem extends StatelessWidget {
 const StockListItem({Key? key, required this.stock}) : super(key: key);

 final Stock stock;

 @override
 Widget build(BuildContext context) {
   final textTheme = Theme.of(context).textTheme;
   const cutoff = 150;
   final bool haveUrl = (stock.thumbUrl != '');
   return Material(
     child: ListTile(
       leading: SizedBox(
         child: ClipOval(
           child: Material(
             color: Colors.lightBlue.withOpacity(0.25),
             child: Center(
               child: (haveUrl)
                   ? Hero(
                       tag: stock.id,
                       child: FadeInImage.memoryNetwork(
                         placeholder: kTransparentImage,
                         image: stock.thumbUrl,
                         fit: BoxFit.cover,
                         width: 48,
                         height: 48,
                       ),
                       flightShuttleBuilder: (
                         BuildContext flightContext,
                         Animation<double> animation,
                         HeroFlightDirection flightDirection,
                         BuildContext fromHeroContext,
                         BuildContext toHeroContext,
                       ) {
                         return Container(
                           decoration: BoxDecoration(
                             shape: BoxShape.circle,
                             image: DecorationImage(
                               image: NetworkImage(stock.thumbUrl),
                             ),
                           ),
                         );
                       },
                     )
                   : Text('${stock.id.toString()} ${stock.title}'),
             ),
           ),
         ),
         width: 48,
         height: 48,
       ),
       title: Text(stock.title),
       isThreeLine: true,
       subtitle: Text((stock.description.length <= cutoff)
           ? stock.description
           : '${stock.description.substring(0, cutoff)}...'),
       dense: true,
     ),
   );
 }
}

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:agent_01/stocks/stocks.dart';
import 'package:http/http.dart' as http;
import 'package:transparent_image/transparent_image.dart';
import 'package:agent_01/CustomWidgets/AscHero.dart';

class StockPage extends StatelessWidget {
  const StockPage({Key? key}) : super(key: key);
  static const routeName = '/stocks/stock';

  @override
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments as ScreenArguments;
    final Stock _stock = args.stock;
    //final bool haveUrl = (_stock.thumbUrl != '');
    return Scaffold(
      appBar: AppBar(
          title: Row(
        children: [
          AscHero(
              thumbUrl: _stock.thumbUrl, tag: _stock.id, title: _stock.title),
          const SizedBox(
            width: 12,
            height: 1,
          ),
          Text(_stock.title),
        ],
      )),
      body: Center(
        child: Text(_stock.description),
      ),
    );
  }
}

class ScreenArguments {
  final Stock stock;

  ScreenArguments(this.stock);
}

,但下面的动画不起作用,图像消失并重新出现在新视图中:

(菜单屏幕)

import 'package:agent_01/CustomWidgets/AscHero.dart';
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Menu'),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: GridView.count(
            crossAxisCount: 3,
            children: const [
              MenuGrp('stocks', 'Properties'),
              MenuGrp('clients', 'Clients'),
              MenuGrp('checkin', 'Checkin'),
              MenuGrp('calendar', 'Calendar'),
              MenuGrp('viewings', 'Viewings'),
              MenuGrp('offers', 'Offers'),
              MenuGrp('reports', 'Reports'),
              MenuGrp('calculators', 'Calculators'),
            ],
          ),
        ),
      ),
    );
  }
}

class MenuGrp extends StatelessWidget {
  final String indexStr;
  final String labelStr;

  const MenuGrp(this.indexStr, this.labelStr, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final String imagePath = 'assets/images/$indexStr.png';
    final AssetImage assetImage = AssetImage(imagePath);
    return Material(
      child: InkWell(
        onTap: () => Navigator.pushNamed(context, '/' + indexStr),
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            children: [
              // Expanded(child: MenuButton(indexStr)),
              AscHero(
                title: labelStr,
                tag: indexStr,
                assetImage: assetImage,
              ),
              SizedBox(
                  child: Text(
                labelStr,
                style: const TextStyle(fontWeight: FontWeight.bold),
              )),
            ],
          ),
        ),
        splashColor: Colors.lightBlue.withOpacity(0.25),
        borderRadius: BorderRadius.circular(100),
      ),
      color: Colors.white,
    );
  }
}

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:agent_01/stocks/stocks.dart';
import 'package:http/http.dart' as http;
import 'package:agent_01/CustomWidgets/AscHero.dart';

class StocksPage extends StatelessWidget {
  const StocksPage({Key? key}) : super(key: key);
  static const indexStr = 'stocks';
  static const labelStr = 'Properties';

  @override
  Widget build(BuildContext context) {
    const String imagePath = 'assets/images/$indexStr.png';
    var assetImage = const AssetImage(imagePath);
    return Scaffold(
      appBar: AppBar(
        title: Row(
          children: [
            AscHero(
                assetImage: assetImage,
                tag: indexStr,
                title: labelStr,
                radius: 32),
            const SizedBox(
              width: 12,
              height: 1,
            ),
            const Text(labelStr),
          ],
        ),
      ),
      body: BlocProvider(
        create: (_) =>
            StockBloc(httpClient: http.Client())..add(StockFetched()),
        child: StocksList(),
      ),
    );
  }
}

问题出在 AscHero class 中的 flightShuttleBuilder;即使只提供了资产图像,它也试图使用网络图像。经过一些实验后,我开始使用它:

flightShuttleBuilder: (
  BuildContext flightContext,
  //...
) {
  return ClipOval(
    child: (haveUrl)
        ? Image(
            image: NetworkImage(thumbUrl),
          )
        : Image(image: assetImage),
  );
},