Created
May 23, 2025 19:49
-
-
Save kururu-abdo/2fad37bc9ed25da77faef988836859a5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Product Gallery Demo', | |
theme: ThemeData( | |
primarySwatch: Colors.green, | |
fontFamily: 'Inter', // Assuming Inter font is available or a similar system font | |
), | |
home: Scaffold( | |
appBar: AppBar( | |
title: const Text('Product Image Gallery'), | |
), | |
body: Center( | |
child: ProductImageGallery( | |
imageUrls: const [ | |
'https://placehold.co/600x400/CCCCCC/FFFFFF?text=Goat+1', | |
'https://placehold.co/600x400/AAAAAA/FFFFFF?text=Goat+2', | |
'https://placehold.co/600x400/BBBBBB/FFFFFF?text=Goat+3', | |
'https://placehold.co/600x400/DDDDDD/FFFFFF?text=Goat+4', | |
], | |
), | |
), | |
), | |
); | |
} | |
} | |
class ProductImageGallery extends StatefulWidget { | |
final List<String> imageUrls; | |
const ProductImageGallery({ | |
super.key, | |
required this.imageUrls, | |
}); | |
@override | |
State<ProductImageGallery> createState() => _ProductImageGalleryState(); | |
} | |
class _ProductImageGalleryState extends State<ProductImageGallery> { | |
final PageController _pageController = PageController(); | |
int _currentIndex = 0; | |
@override | |
void initState() { | |
super.initState(); | |
// Listen to page changes to update the current index for the indicator and thumbnails | |
_pageController.addListener(() { | |
setState(() { | |
_currentIndex = _pageController.page!.round(); | |
}); | |
}); | |
} | |
@override | |
void dispose() { | |
_pageController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
mainAxisSize: MainAxisSize.min, // Take minimum vertical space | |
children: [ | |
// Main image section with PageView for swiping and "1/X" indicator | |
Stack( | |
children: [ | |
SizedBox( | |
height: 300, // Fixed height for the main image display area | |
width: MediaQuery.of(context).size.width, // Take full width | |
child: PageView.builder( | |
controller: _pageController, | |
itemCount: widget.imageUrls.length, | |
itemBuilder: (context, index) { | |
return Image.network( | |
widget.imageUrls[index], | |
fit: BoxFit.cover, // Cover the entire space | |
width: double.infinity, | |
// Fallback for image loading errors, showing a grey box with a broken image icon | |
errorBuilder: (context, error, stackTrace) { | |
return Container( | |
color: Colors.grey[300], | |
child: const Icon(Icons.broken_image, size: 50, color: Colors.grey), | |
); | |
}, | |
); | |
}, | |
), | |
), | |
// "1/X" indicator positioned at the bottom right | |
Positioned( | |
bottom: 16.0, | |
right: 16.0, | |
child: Container( | |
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), | |
decoration: BoxDecoration( | |
color: Colors.black54, // Semi-transparent black background | |
borderRadius: BorderRadius.circular(8.0), // Rounded corners for the indicator | |
), | |
child: Text( | |
'${_currentIndex + 1}/${widget.imageUrls.length}', // Display current index out of total | |
style: const TextStyle( | |
color: Colors.white, | |
fontSize: 12.0, | |
), | |
), | |
), | |
), | |
], | |
), | |
const SizedBox(height: 16.0), // Spacing between main image and thumbnails | |
// Thumbnail gallery section | |
SizedBox( | |
height: 80, // Fixed height for the horizontal thumbnail list | |
child: ListView.builder( | |
scrollDirection: Axis.horizontal, // Horizontal scrolling | |
itemCount: widget.imageUrls.length, | |
itemBuilder: (context, index) { | |
return GestureDetector( | |
onTap: () { | |
// Animate to the selected page when a thumbnail is tapped | |
_pageController.animateToPage( | |
index, | |
duration: const Duration(milliseconds: 300), // Smooth animation duration | |
curve: Curves.ease, // Animation curve | |
); | |
}, | |
child: Container( | |
width: 80, // Fixed width for each thumbnail | |
height: 80, // Fixed height for each thumbnail | |
margin: const EdgeInsets.symmetric(horizontal: 4.0), // Spacing between thumbnails | |
decoration: BoxDecoration( | |
borderRadius: BorderRadius.circular(8.0), // Rounded corners for thumbnails | |
border: Border.all( | |
// Highlight the currently active thumbnail with a green border | |
color: _currentIndex == index ? Colors.green : Colors.transparent, | |
width: 3.0, | |
), | |
), | |
child: ClipRRect( | |
borderRadius: BorderRadius.circular(8.0), // Clip image to rounded corners | |
child: Image.network( | |
widget.imageUrls[index], | |
fit: BoxFit.cover, // Cover the thumbnail area | |
// Fallback for image loading errors | |
errorBuilder: (context, error, stackTrace) { | |
return Container( | |
color: Colors.grey[300], | |
child: const Icon(Icons.broken_image, size: 30, color: Colors.grey), | |
); | |
}, | |
), | |
), | |
), | |
); | |
}, | |
), | |
), | |
], | |
); | |
} | |
} | |
// The ProductCard widget from the previous interaction is kept here for reference. | |
// You can integrate this ProductImageGallery into the ProductCard or use them separately. | |
class ProductCard extends StatelessWidget { | |
final String imageUrl; | |
final String productName; | |
final String price; | |
final String weight; | |
final bool isHealthy; | |
const ProductCard({ | |
super.key, | |
required this.imageUrl, | |
required this.productName, | |
required this.price, | |
required this.weight, | |
this.isHealthy = false, | |
}); | |
@override | |
Widget build(BuildContext context) { | |
return Card( | |
// Apply rounded corners to the card | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(16.0), | |
), | |
// Add a slight elevation for a subtle shadow effect | |
elevation: 4.0, | |
margin: const EdgeInsets.all(16.0), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
mainAxisSize: MainAxisSize.min, // Make the column take minimum space | |
children: [ | |
// Image section | |
ClipRRect( | |
borderRadius: const BorderRadius.vertical(top: Radius.circular(16.0)), | |
child: Image.network( | |
imageUrl, | |
height: 200, | |
width: double.infinity, | |
fit: BoxFit.cover, | |
// Fallback for image loading errors | |
errorBuilder: (context, error, stackTrace) { | |
return Container( | |
height: 200, | |
width: double.infinity, | |
color: Colors.grey[300], | |
child: const Icon(Icons.broken_image, size: 50, color: Colors.grey), | |
); | |
}, | |
), | |
), | |
// "Guaranteed Healthy" tag | |
if (isHealthy) | |
Container( | |
width: double.infinity, | |
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0), | |
decoration: const BoxDecoration( | |
color: Colors.green, | |
// Only bottom corners are rounded if card is not fully rounded | |
// If card is fully rounded, this might not need specific rounding here | |
), | |
child: Row( | |
children: const [ | |
Icon(Icons.assignment, color: Colors.white, size: 20), // Icon for guaranteed healthy | |
SizedBox(width: 8.0), | |
Text( | |
'Guaranteed Healthy', | |
style: TextStyle( | |
color: Colors.white, | |
fontWeight: FontWeight.bold, | |
fontSize: 14.0, | |
), | |
), | |
], | |
), | |
), | |
// Product details section | |
Padding( | |
padding: const EdgeInsets.all(16.0), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: [ | |
Text( | |
productName, | |
style: const TextStyle( | |
fontSize: 20.0, | |
fontWeight: FontWeight.bold, | |
), | |
), | |
const SizedBox(height: 8.0), | |
Text( | |
price, | |
style: TextStyle( | |
fontSize: 22.0, | |
fontWeight: FontWeight.bold, | |
color: Colors.grey[800], | |
), | |
), | |
const SizedBox(height: 12.0), | |
Row( | |
children: [ | |
Icon(Icons.scale, color: Colors.grey[600], size: 20), // Scale icon for weight | |
const SizedBox(width: 8.0), | |
Text( | |
weight, | |
style: TextStyle( | |
fontSize: 16.0, | |
color: Colors.grey[600], | |
), | |
), | |
], | |
), | |
], | |
), | |
), | |
], | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment