Creating a Portfolio Carousel Widget with Image Overlay and Caption in Flutter
Flutter offers powerful UI tools to build engaging and dynamic user interfaces. A common requirement for portfolio websites or apps is a carousel to showcase projects, images, or products. This article will guide you through creating a sophisticated portfolio carousel widget in Flutter, complete with image display, an elegant overlay, and descriptive captions.
Our goal is to build a carousel where each slide features an image, and on top of that image, there's a semi-transparent overlay containing a title and a description. Users will be able to swipe through these portfolio items seamlessly.
Core Concepts and Flutter Widgets
To achieve this, we'll leverage several key Flutter widgets:
PageView: The foundation of our carousel, allowing users to swipe horizontally between pages.Stack: Essential for layering widgets. We'll use it to place the image at the bottom, and the overlay and text on top.Positioned: Used within aStackto precisely control the placement of its children.Container: For creating the background overlay with styling (color, opacity, padding).Image.assetorImage.network: To display local or network images.Text: To render the title and caption.PageController: To programmatically control thePageViewand observe page changes.
Step-by-Step Implementation
Let's break down the process into manageable steps.
1. Define the Data Model
First, we need a simple data structure to represent each portfolio item. This will hold the image path, title, and a brief description.
class PortfolioItem {
final String imageUrl;
final String title;
final String description;
PortfolioItem({
required this.imageUrl,
required this.title,
required this.description,
});
}
2. Create the Portfolio Carousel Widget
This will be a StatefulWidget to manage the current page index. It will contain the PageView and the list of portfolio items.
import 'package:flutter/material.dart';
// You would typically define PortfolioItem in a separate file or at the top of this file.
// For this example, let's assume it's available.
// class PortfolioItem { ... }
class PortfolioCarouselWidget extends StatefulWidget {
final List items;
const PortfolioCarouselWidget({Key? key, required this.items}) : super(key: key);
@override
_PortfolioCarouselWidgetState createState() => _PortfolioCarouselWidgetState();
}
class _PortfolioCarouselWidgetState extends State {
late PageController _pageController;
int _currentPage = 0;
@override
void initState() {
super.initState();
_pageController = PageController(viewportFraction: 1.0); // Full width items
_pageController.addListener(() {
setState(() {
// Round the page value to get the current integer page index
// This is useful if viewportFraction is less than 1.0, or for precise tracking
_currentPage = _pageController.page!.round();
});
});
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 300, // Fixed height for the carousel, adjust as needed
child: PageView.builder(
controller: _pageController,
itemCount: widget.items.length,
itemBuilder: (context, index) {
return _buildPortfolioCard(widget.items[index]);
},
),
);
}
Widget _buildPortfolioCard(PortfolioItem item) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), // Spacing between cards
child: ClipRRect(
borderRadius: BorderRadius.circular(12.0), // Rounded corners for the card
child: Stack(
children: [
// Background Image
// Using Image.asset assuming local assets. For network images, use Image.network.
Image.asset(
item.imageUrl,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
),
// Image Overlay and Caption
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7), // Semi-transparent black at the bottom
],
stops: const [0.6, 1.0], // Gradient starts at 60% down
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.end, // Align content to the bottom
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
item.description,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 16,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
),
],
),
),
);
}
}
3. Usage Example (Main Application)
To see your carousel in action, you'll need to prepare some images (e.g., in your assets/images folder) and then integrate the widget into your main application structure. Make sure your PortfolioItem class is accessible.
import 'package:flutter/material.dart';
// Assuming PortfolioItem and PortfolioCarouselWidget are in a file named portfolio_carousel_widget.dart
// import 'path_to_your_widget/portfolio_carousel_widget.dart';
// Re-defining PortfolioItem here for a self-contained example.
class PortfolioItem {
final String imageUrl;
final String title;
final String description;
PortfolioItem({
required this.imageUrl,
required this.title,
required this.description,
});
}
// Re-defining PortfolioCarouselWidget here for a self-contained example.
// In a real app, these would be imported from separate files.
class PortfolioCarouselWidget extends StatefulWidget {
final List items;
const PortfolioCarouselWidget({Key? key, required this.items}) : super(key: key);
@override
_PortfolioCarouselWidgetState createState() => _PortfolioCarouselWidgetState();
}
class _PortfolioCarouselWidgetState extends State {
late PageController _pageController;
int _currentPage = 0;
@override
void initState() {
super.initState();
_pageController = PageController(viewportFraction: 1.0);
_pageController.addListener(() {
setState(() {
_currentPage = _pageController.page!.round();
});
});
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 300,
child: PageView.builder(
controller: _pageController,
itemCount: widget.items.length,
itemBuilder: (context, index) {
return _buildPortfolioCard(widget.items[index]);
},
),
);
}
Widget _buildPortfolioCard(PortfolioItem item) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(12.0),
child: Stack(
children: [
Image.asset(
item.imageUrl,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
),
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7),
],
stops: const [0.6, 1.0],
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
item.description,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 16,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
),
],
),
),
);
}
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Portfolio Carousel Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// Dummy data for portfolio items
final List portfolioItems = [
PortfolioItem(
imageUrl: 'assets/image1.jpg', // Make sure to add images to pubspec.yaml
title: 'Project Alpha',
description: 'A cutting-edge mobile application for productivity enthusiasts.',
),
PortfolioItem(
imageUrl: 'assets/image2.jpg',
title: 'Website Redesign',
description: 'Modern and responsive web design for an e-commerce platform.',
),
PortfolioItem(
imageUrl: 'assets/image3.jpg',
title: 'UI/UX Case Study',
description: 'Detailed analysis and design solutions for a fintech app.',
),
];
return Scaffold(
appBar: AppBar(
title: const Text('My Portfolio'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Featured Works:',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
PortfolioCarouselWidget(items: portfolioItems),
// You can add more widgets here, like page indicators based on _currentPage
],
),
),
);
}
}
Remember to update your pubspec.yaml file to include your assets. Create an assets folder in your project root and place your images there.
flutter:
uses-material-design: true
assets:
- assets/image1.jpg
- assets/image2.jpg
- assets/image3.jpg
# Add all your image paths here
Explanation of Key Parts
Let's delve into the important aspects of the code:
PortfolioItemData Model: A simple class to keep your data organized, making it easy to pass project details around._PortfolioCarouselWidgetState:PageController: Initialized withviewportFraction: 1.0to ensure each page takes up the full width of thePageView. A listener is added to update_currentPagewhen the user swipes, which can be useful for adding page indicators later.PageView.builder: Efficiently builds only the visible pages.itemBuilderconstructs each slide using the_buildPortfolioCardmethod.
_buildPortfolioCard(PortfolioItem item):PaddingandClipRRect: Adds some horizontal spacing between carousel items and gives them rounded corners, enhancing visual appeal.Stack: The core for layering. TheImage.assetfills the background, ensuring the portfolio item's image is prominent.Positioned.fill: This widget makes its child (theContaineracting as the overlay) expand to fill the entire available space of theStack.- Overlay
ContainerwithLinearGradient: Instead of a solid color overlay, we use aLinearGradient. It starts transparent at the top and transitions to a semi-transparent black at the bottom. This allows the top part of the image to be fully visible while providing a clear, readable background for the text at the bottom. Thestopsproperty controls where the color transition occurs. PaddingandColumnfor Text: Inside the overlay,Paddingensures the text isn't cramped. AColumnwithmainAxisAlignment: MainAxisAlignment.endpushes the title and description to the bottom of the overlay, creating a modern layout. Styling ensures readability with white text against the dark gradient.
Conclusion
You have now successfully created a professional and highly customizable portfolio carousel widget in Flutter. This widget is a fantastic way to showcase your projects with visual flair, making your application or website more engaging for users. You can further enhance this widget by adding dot indicators, autoplay functionality, or even dynamic animations for the overlay and text based on page transitions.