接續上 2 篇所需要的元件~~。
今日的程式碼 => GITHUB
來補充前面兩篇所需要的元件。
https://github.com/wayne900204/Flutter-IT30/tree/main/day_25/images
/// 每一個 Category Section 的樣子。
class CategorySection extends StatelessWidget {
TextTheme _textTheme(context) => Theme.of(context).textTheme;
const CategorySection({
Key? key,
required this.category,
}) : super(key: key);
final Category category;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
margin: const EdgeInsets.only(bottom: 16),
color: scheme.surface,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTileHeader(context),
_buildFoodTileList(context),
],
),
);
}
/// Section Title
Widget _buildFoodTileList(BuildContext context) {
return Column(
children: List.generate(
category.foods.length,
(index) {
final food = category.foods[index];
bool isLastIndex = index == category.foods.length - 1;
return _buildFoodTile(
food: food,
context: context,
isLastIndex: isLastIndex,
);
},
),
);
}
/// FSection Header
Widget _buildSectionTileHeader(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
_sectionTitle(context),
const SizedBox(height: 8.0),
category.subtitle != null
? _sectionSubtitle(context)
: const SizedBox(),
const SizedBox(height: 16),
],
);
}
/// Section Title 的 title
Widget _sectionTitle(BuildContext context) {
return Row(
children: [
if (category.isHotSale) _buildSectionHotSaleIcon(),
Text(
category.title,
style: _textTheme(context).headline6,
strutStyle: Helper.buildStrutStyle(_textTheme(context).headline6),
// strutStyle: Helper.buildStrutStyle(_textTheme(context).headline6),
)
],
);
}
/// section Title 的 subTitle
Widget _sectionSubtitle(BuildContext context) {
return Text(
category.subtitle!,
style: _textTheme(context).subtitle2,
strutStyle: Helper.buildStrutStyle(_textTheme(context).subtitle2),
);
}
/// Section body 的 Food 樣式。
Widget _buildFoodTile({
required BuildContext context,
required bool isLastIndex,
required Food food,
}) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildFoodDetail(food: food, context: context),
_buildFoodImage(food.imageUrl),
],
),
!isLastIndex ? const Divider(height: 16.0) : const SizedBox(height: 8.0)
],
);
}
/// food Image
Widget _buildFoodImage(String url) {
return FadeInImage.assetNetwork(
placeholder: 'images/transparent.png',
image: url,
width: 64,
);
}
/// food Detail
Widget _buildFoodDetail({
required BuildContext context,
required Food food,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(food.name, style: _textTheme(context).subtitle1),
const SizedBox(height: 16),
Row(
children: [
Text(
"特價" + food.price + " ",
style: _textTheme(context).caption,
strutStyle: Helper.buildStrutStyle(_textTheme(context).caption),
),
Text(
food.comparePrice,
strutStyle: Helper.buildStrutStyle(_textTheme(context).caption),
style: _textTheme(context)
.caption
?.copyWith(decoration: TextDecoration.lineThrough),
),
const SizedBox(width: 8.0),
if (food.isHotSale) _buildFoodHotSaleIcon(),
],
),
],
);
}
/// Section HotSale Icon
Widget _buildSectionHotSaleIcon() {
return Container(
margin: const EdgeInsets.only(right: 4.0),
child: Icon(
Icons.whatshot,
color: scheme.primary,
size: 20.0,
),
);
}
/// Food HotSale Icon
Widget _buildFoodHotSaleIcon() {
return Container(
child: Icon(Icons.whatshot, color: scheme.primary, size: 16.0),
padding: const EdgeInsets.all(4.0),
decoration: BoxDecoration(
color: scheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(16.0),
),
);
}
}
/// 餐廳的圖片形狀
class CustomShape extends CustomClipper<Path> {
@override
getClip(Size size) {
double height = size.height;
double width = size.width;
var path = Path();
path.lineTo(0, height - 30);
path.quadraticBezierTo(width / 2, height, width, height - 30);
path.lineTo(width, 0);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper oldClipper) => true;
}
/// 折扣卡片
class DiscountCard extends StatelessWidget {
const DiscountCard({
Key? key,
required this.title,
required this.subtitle,
}) : super(key: key);
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
var textTheme = Theme.of(context).textTheme;
return Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 16.0,
),
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 20,
),
decoration: BoxDecoration(
color: scheme.primary,
borderRadius: BorderRadius.circular(10.0),
image: DecorationImage(
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
scheme.primary.withOpacity(0.08),
BlendMode.dstATop,
),
image: AssetImage('images/pattern.png'),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: textTheme.subtitle1
?.copyWith(color: scheme.surface, fontWeight: FontWeight.w600),
),
Text(
subtitle,
style: textTheme.bodyText2?.copyWith(
color: scheme.surface,
),
),
],
),
);
}
}
/// AppBar 的按鈕
class FIconButton extends StatelessWidget {
const FIconButton({
Key? key,
required this.iconData,
required this.onPressed,
}) : super(key: key);
final IconData iconData;
final void Function() onPressed;
@override
Widget build(BuildContext context) {
return Center(
child: IconButton(
splashColor: Colors.transparent,
onPressed: ()=>onPressed,
icon: Container(
height: 48,
width: 48,
decoration: buildBoxDecoration(),
child: Icon(
iconData,
color: scheme.primary,
size: 20,
),
),
),
);
}
BoxDecoration buildBoxDecoration() {
return BoxDecoration(
shape: BoxShape.circle,
color: scheme.surface,
);
}
}
class FlutterHead extends StatelessWidget {
const FlutterHead({
Key? key,
}) : super(key: key);
final imageUrl =
'https://flutter.dev/assets/images/shared/brand/flutter/logo/flutter-lockup.png';
@override
Widget build(BuildContext context) {
return Positioned(
bottom: -5,
right: 10.0,
child: FadeInImage.assetNetwork(
placeholder: 'images/transparent.png',
image: imageUrl,
width: 150,
height: 72,
fit: BoxFit.fitWidth,
),
);
}
}
/// App 餐廳圖片的地方
class HeaderClip extends StatelessWidget {
const HeaderClip({
Key? key,
required this.data,
required this.context,
}) : super(key: key);
final PageData data;
final BuildContext context;
@override
Widget build(BuildContext _) {
final textTheme = Theme.of(context).textTheme;
return ClipPath(
clipper: CustomShape(),
child: Stack(
children: [
Container(
height: 275,
color: scheme.primary.withOpacity(0.3),
child: FadeInImage.assetNetwork(
placeholder: 'images/transparent.png',
image: data.backgroundUrl,
width: double.infinity,
fit: BoxFit.fitWidth,
),
),
// 目的 讓顏色變暗。
Container(
height: 275,
color: scheme.secondary.withOpacity(0.7),
),
Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).viewPadding.top + kToolbarHeight,
),
child: Column(
children: [
Text(
data.title,
style: textTheme.headline5?.copyWith(
color: scheme.surface,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8.0),
Center(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 4.0,
),
decoration: BoxDecoration(
border: Border.all(color: scheme.surface),
borderRadius: BorderRadius.circular(50),
),
child: Text(
"抵達時間: " + data.deliverTime,
style: textTheme.caption?.copyWith(color: scheme.surface),
strutStyle: StrutStyle(forceStrutHeight: true),
),
),
),
const SizedBox(height: 8.0),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.star_rate_rounded,
size: 16,
color: scheme.surface,
),
const SizedBox(width: 4.0),
Text(
data.rate.toString(),
style: textTheme.caption?.copyWith(
fontWeight: FontWeight.bold,
color: scheme.surface,
),
),
const SizedBox(width: 4.0),
Text(
"(" + data.rateQuantity.toString() + ")",
style: textTheme.caption?.copyWith(
color: scheme.surface,
),
),
],
)
],
),
)
],
),
);
}
}
/// 宣傳框
class PromoText extends StatelessWidget {
const PromoText({
Key? key,
required this.title,
}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
var textTheme = Theme.of(context).textTheme;
// 這邊是距離上一層的 Stack 的 bottom left right
return Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 48,
),
width: double.infinity,
color: scheme.primary.withOpacity(0.1),
child: Text(
title,
style: textTheme.bodyText1?.copyWith(color: scheme.primary),
),
),
);
}
}