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),
);
},
我正在尝试创建一个 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),
);
},