I'm trying to create a re-usable hero widget, in which either an asset image or web image might be used in different contexts. Currently, where I use network images it works as expected, e.g. having the custom widget as Leading in a ListView item, animating into the title of the AppBar when that ListView item is selected, as well as on pop-ing that view to return to the ListView.
However, where I use a local asset combined with my custom AscHero widget, the hero animation doesn't happen.
('m still very new to Flutter/Dart and OOP for that matter so feel free to point out where I'm doing sub-optimal or plain stupid things :)
The bespoke widget:
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,
);
}
}
what works:
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,
),
);
}
}
and
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);
}
, but the animation between the below doesn't work, with the image dissapearing and reappearing on the new view:
(the menu screen)
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,
);
}
}
and
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(),
),
);
}
}
The problem was the flightShuttleBuilder
in the AscHero
class; it was trying to use a network image even when only an asset image was supplied. After some experimentation I got it working with :
flightShuttleBuilder: (
BuildContext flightContext,
//...
) {
return ClipOval(
child: (haveUrl)
? Image(
image: NetworkImage(thumbUrl),
)
: Image(image: assetImage),
);
},