flipbook_pro 1.0.2
flipbook_pro: ^1.0.2 copied to clipboard
A Flutter package that provides beautiful Flipboard-style vertical page flip animations with smooth 3D perspective transformations and spring physics.
import 'package:flutter/material.dart';
import 'package:flipbook_pro/flipbook_pro.dart';
void main() {
runApp(const FlipboardDemoApp());
}
class FlipboardDemoApp extends StatelessWidget {
const FlipboardDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flipboard Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
useMaterial3: true,
),
home: const FlipboardDemo(),
);
}
}
class FlipboardDemo extends StatefulWidget {
const FlipboardDemo({super.key});
@override
State<FlipboardDemo> createState() => _FlipboardDemoState();
}
class _FlipboardDemoState extends State<FlipboardDemo>
with SingleTickerProviderStateMixin {
bool _showSplash = true;
late AnimationController _bounceController;
late Animation<double> _bounceAnimation;
@override
void initState() {
super.initState();
// Bounce animasyonu
_bounceController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_bounceAnimation = Tween<double>(begin: -15, end: 15).animate(
CurvedAnimation(
parent: _bounceController,
curve: Curves.easeInOut,
),
);
// Sürekli yukarı-aşağı
_bounceController.repeat(reverse: true);
// 3 saniye sonra splash'ı kapat
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() {
_showSplash = false;
});
_bounceController.stop();
}
});
}
@override
void dispose() {
_bounceController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// Ana içerik
FlipboardView(
itemCount: 3,
initialPage: 0,
itemBuilder: (context, index) {
if (index == 0) {
return const Page1FullImage();
} else if (index == 1) {
return const Page2NewsCards();
} else {
return const Page3BusinessArticle();
}
},
loop: true,
perspective: 0.001,
),
// Splash overlay
if (_showSplash)
AnimatedOpacity(
opacity: _showSplash ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
child: Container(
color: Colors.black.withValues(alpha: 0.9),
child: Center(
child: AnimatedBuilder(
animation: _bounceAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _bounceAnimation.value),
child: child,
);
},
child: Image.asset(
'assets/images/scrolling.png',
width: 80,
height: 80,
color: Colors.white,
),
),
),
),
),
],
),
);
}
}
// ============ SAYFA 1: Tam Sayfa Manzara Resmi ============
class Page1FullImage extends StatelessWidget {
const Page1FullImage({super.key});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: Column(
children: [
// Header
const FlipboardHeader(),
// Tam sayfa resim
Expanded(
child: Stack(
fit: StackFit.expand,
children: [
Image.asset('assets/images/image.jpg', fit: BoxFit.cover),
// Alt gradient
Positioned(
bottom: 0,
left: 0,
right: 0,
height: 120,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.9),
],
),
),
),
),
// Watermark (sağ alt)
Positioned(
bottom: 16,
right: 16,
child: Text(
'© Peter König',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6),
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
),
// Alt bilgi
Positioned(
bottom: 12,
left: 16,
right: 80,
child: Text(
'Tübingen at blue hour · 500px.com · 500px.com',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.85),
fontSize: 14,
),
),
),
],
),
),
// Author satırı
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
// Profil resmi
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.grey[700]!, width: 1),
),
child: ClipOval(
child: Icon(
Icons.person,
color: Colors.grey[500],
size: 24,
),
),
),
const SizedBox(width: 12),
const Text(
'Peter König',
style: TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
const Spacer(),
Icon(Icons.more_vert, color: Colors.grey[600], size: 24),
],
),
),
// Action bar
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Icon(Icons.favorite_border, color: Colors.grey[500], size: 26),
const SizedBox(width: 6),
Text(
'1',
style: TextStyle(color: Colors.grey[500], fontSize: 14),
),
const SizedBox(width: 24),
Icon(
Icons.chat_bubble_outline,
color: Colors.grey[500],
size: 24,
),
const SizedBox(width: 24),
Icon(Icons.add, color: Colors.grey[500], size: 26),
const Spacer(),
Icon(Icons.share_outlined, color: Colors.grey[500], size: 24),
],
),
),
],
),
);
}
}
// ============ SAYFA 2: Haber Kartları ============
class Page2NewsCards extends StatefulWidget {
const Page2NewsCards({super.key});
@override
State<Page2NewsCards> createState() => _Page2NewsCardsState();
}
class _Page2NewsCardsState extends State<Page2NewsCards> {
// Like durumları
final Map<int, bool> _likedNews = {};
final Map<int, int> _likeCounts = {0: 12, 1: 5};
void _toggleLike(int index) {
setState(() {
_likedNews[index] = !(_likedNews[index] ?? false);
if (_likedNews[index]!) {
_likeCounts[index] = (_likeCounts[index] ?? 0) + 1;
} else {
_likeCounts[index] = (_likeCounts[index] ?? 1) - 1;
}
});
}
void _openNews(
BuildContext context,
String title,
String source,
String description,
String imagePath,
) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NewsDetailPage(
title: title,
source: source,
description: description,
imagePath: imagePath,
),
),
);
}
void _showSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 1),
backgroundColor: Colors.grey[800],
),
);
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: Column(
children: [
const FlipboardHeader(),
Expanded(
child: ListView(
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
children: [
// Haber 1
_buildNewsCard(
index: 0,
title:
"İmamoğlu: 'Kreş bağışları rüşvet sayıldı, bu yalanlar yüzünden tutsağız'",
source: 'cumhuriyet.com.tr',
time: '3s',
description:
'Ekrem İmamoğlu, hakkında yürütülen soruşturmaya ilişkin açıklamalarda bulundu. İmamoğlu, Cumhurbaşkanlığı Aday Ofisi\'nin sosyal medya hesabından paylaşılan mesajında şu ifadeleri ...',
imagePath: 'assets/images/thumb1.webp',
publisher: 'Cumhuriyet',
publisherColor: Colors.red,
flipTo: 'Cumhuriyet Türkiye',
),
Container(height: 0.5, color: Colors.grey[800]),
// Haber 2
_buildNewsCard(
index: 1,
title:
"Cumhurbaşkanlığı Başdanışmanlığı'na yeni isim: Üst düzey atamalar Resmî Gazete'de",
source: 'Halktv.com.tr',
time: '2s',
subtitle: 'Hab...',
description:
'Resmi Gazete\'de yayımlanan Cumhurbaşkanlığı atama kararlarıyla, kamu yönetiminde geniş kapsamlı bir görev ...',
imagePath: 'assets/images/thumb2.webp',
publisher: 'Halktv.com.tr',
publisherColor: Colors.red,
flipTo: 'Halk Tv Haber Portalı',
),
],
),
),
],
),
);
}
Widget _buildNewsCard({
required int index,
required String title,
required String source,
required String time,
String? subtitle,
required String description,
required String imagePath,
required String publisher,
required Color publisherColor,
required String flipTo,
}) {
final isLiked = _likedNews[index] ?? false;
final likeCount = _likeCounts[index] ?? 0;
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => _openNews(context, title, source, description, imagePath),
splashColor: Colors.white.withValues(alpha: 0.1),
highlightColor: Colors.white.withValues(alpha: 0.05),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Başlık
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
height: 1.25,
),
),
const SizedBox(height: 12),
// İçerik + resim
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$source · $time · ${subtitle ?? '...'}',
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
),
),
const SizedBox(height: 8),
Text(
description,
style: TextStyle(
color: Colors.grey[400],
fontSize: 14,
height: 1.5,
),
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: 12),
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
imagePath,
width: 110,
height: 85,
fit: BoxFit.cover,
),
),
],
),
const SizedBox(height: 16),
// Publisher
Row(
children: [
// Publisher avatar
GestureDetector(
onTap: () =>
_showSnackBar(context, '$publisher takip edildi'),
child: Stack(
children: [
CircleAvatar(
radius: 18,
backgroundColor: publisherColor,
child: Text(
publisher.substring(0, 1),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: 2),
),
child: const Icon(
Icons.add,
color: Colors.white,
size: 10,
),
),
),
],
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
publisher,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
),
Text(
'şuraya flipledi: $flipTo',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
GestureDetector(
onTap: () => _showSnackBar(context, 'Daha fazla seçenek'),
child: Icon(
Icons.more_vert,
color: Colors.grey[700],
size: 22,
),
),
],
),
const SizedBox(height: 12),
// Action bar
Row(
children: [
GestureDetector(
onTap: () => _toggleLike(index),
child: Row(
children: [
Icon(
isLiked ? Icons.favorite : Icons.favorite_border,
color: isLiked ? Colors.red : Colors.grey[600],
size: 24,
),
if (likeCount > 0) ...[
const SizedBox(width: 4),
Text(
'$likeCount',
style: TextStyle(
color: isLiked ? Colors.red : Colors.grey[600],
fontSize: 14,
),
),
],
],
),
),
const SizedBox(width: 24),
GestureDetector(
onTap: () => _showSnackBar(context, 'Yorumlar'),
child: Icon(
Icons.chat_bubble_outline,
color: Colors.grey[600],
size: 22,
),
),
const SizedBox(width: 24),
GestureDetector(
onTap: () => _showSnackBar(context, 'Koleksiyona eklendi'),
child: Icon(Icons.add, color: Colors.grey[600], size: 26),
),
const Spacer(),
GestureDetector(
onTap: () => _showSnackBar(context, 'Paylaş'),
child: Icon(
Icons.share_outlined,
color: Colors.grey[600],
size: 22,
),
),
],
),
],
),
),
),
);
}
}
// ============ SAYFA 3: Business Article ============
class Page3BusinessArticle extends StatelessWidget {
const Page3BusinessArticle({super.key});
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black,
child: Column(
children: [
const FlipboardHeader(),
Expanded(
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Hero resim
Stack(
children: [
Image.asset(
'assets/images/newsimage.jpg',
width: double.infinity,
height: 300,
fit: BoxFit.cover,
),
// Category tag
Positioned(
bottom: 16,
left: 16,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'# BUSİNESS',
style: TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
),
),
],
),
// İçerik
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Subscription badge
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lock_outline,
color: Colors.grey[500],
size: 14,
),
const SizedBox(width: 6),
Text(
'Abonelik gerekli olabilir',
style: TextStyle(
color: Colors.grey[500],
fontSize: 12,
),
),
],
),
),
const SizedBox(height: 16),
// Başlık
const Text(
'4 AI Tools to Help You Start a Profitable Solo Business in 2026',
style: TextStyle(
color: Colors.white,
fontSize: 26,
fontWeight: FontWeight.bold,
height: 1.2,
),
),
const SizedBox(height: 14),
// Source
Row(
children: [
Icon(
Icons.bolt,
color: Colors.amber[600],
size: 16,
),
const SizedBox(width: 4),
Text(
'entrepreneur.com · 1s · Ben Angel',
style: TextStyle(
color: Colors.grey[500],
fontSize: 13,
),
),
],
),
const SizedBox(height: 16),
// Description
Text(
"Most AI advice optimizes tasks. This setup replaces leverage. In this video, I break down the four AI tools I use to run a profitable solo business in 2026 — without hiring, without coding and without duct-taping random workflows together. These are not hacks. They're systems you can actually run. Instead of chasing productivity, this stack is built around one idea: Let AI handle the coordination, so you focus on decisions that move revenue. What you'll get: Market ...",
style: TextStyle(
color: Colors.grey[400],
fontSize: 15,
height: 1.6,
),
),
const SizedBox(height: 20),
// Publisher
Row(
children: [
Stack(
children: [
CircleAvatar(
radius: 20,
backgroundColor: Colors.grey[850],
child: const Text(
'E',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
border: Border.all(
color: Colors.black,
width: 2,
),
),
child: const Icon(
Icons.add,
color: Colors.white,
size: 10,
),
),
),
],
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Entrepreneur',
style: TextStyle(
color: Colors.white,
fontSize: 15,
),
),
Text(
'şuraya flipledi: Science & Technology',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
Icon(
Icons.more_vert,
color: Colors.grey[700],
size: 22,
),
],
),
const SizedBox(height: 16),
// Action bar
Row(
children: [
Icon(
Icons.favorite_border,
color: Colors.grey[600],
size: 24,
),
const SizedBox(width: 6),
Text(
'1',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
const SizedBox(width: 24),
Icon(
Icons.chat_bubble_outline,
color: Colors.grey[600],
size: 22,
),
const SizedBox(width: 24),
Icon(Icons.add, color: Colors.grey[600], size: 26),
const SizedBox(width: 6),
Text(
'5',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
const Spacer(),
Icon(
Icons.share_outlined,
color: Colors.grey[600],
size: 22,
),
],
),
],
),
),
],
),
),
),
],
),
);
}
}
// ============ ORTAK BİLEŞENLER ============
class FlipboardHeader extends StatelessWidget {
const FlipboardHeader({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
bottom: false,
child: Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
const Icon(Icons.arrow_back, color: Colors.white, size: 24),
const Spacer(),
const Text(
'SANA ÖZEL',
style: TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
const Spacer(),
Icon(
Icons.more_vert,
color: Colors.white.withValues(alpha: 0.9),
size: 24,
),
],
),
),
);
}
}
// ============ HABER DETAY SAYFASI ============
class NewsDetailPage extends StatelessWidget {
final String title;
final String source;
final String description;
final String imagePath;
const NewsDetailPage({
super.key,
required this.title,
required this.source,
required this.description,
required this.imagePath,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: CustomScrollView(
slivers: [
// App Bar with image
SliverAppBar(
expandedHeight: 250,
pinned: true,
backgroundColor: Colors.black,
leading: IconButton(
icon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
shape: BoxShape.circle,
),
child: const Icon(
Icons.arrow_back,
color: Colors.white,
size: 20,
),
),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
icon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
shape: BoxShape.circle,
),
child: const Icon(Icons.share, color: Colors.white, size: 20),
),
onPressed: () {},
),
],
flexibleSpace: FlexibleSpaceBar(
background: Stack(
fit: StackFit.expand,
children: [
Image.asset(imagePath, fit: BoxFit.cover),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.8),
],
),
),
),
],
),
),
),
// Content
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Source
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(4),
),
child: Text(
source.toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
Text(
'5 dakika önce',
style: TextStyle(color: Colors.grey[500], fontSize: 12),
),
],
),
const SizedBox(height: 16),
// Title
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 26,
fontWeight: FontWeight.bold,
height: 1.3,
),
),
const SizedBox(height: 24),
// Description
Text(
description,
style: TextStyle(
color: Colors.grey[300],
fontSize: 16,
height: 1.7,
),
),
const SizedBox(height: 16),
// Full article text
Text(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\n'
'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\n'
'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.',
style: TextStyle(
color: Colors.grey[400],
fontSize: 16,
height: 1.7,
),
),
const SizedBox(height: 32),
// Action buttons
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildActionButton(Icons.favorite_border, 'Beğen'),
_buildActionButton(Icons.chat_bubble_outline, 'Yorum'),
_buildActionButton(Icons.bookmark_border, 'Kaydet'),
_buildActionButton(Icons.share_outlined, 'Paylaş'),
],
),
const SizedBox(height: 32),
],
),
),
),
],
),
);
}
Widget _buildActionButton(IconData icon, String label) {
return Column(
children: [
Icon(icon, color: Colors.grey[500], size: 26),
const SizedBox(height: 4),
Text(label, style: TextStyle(color: Colors.grey[500], fontSize: 12)),
],
);
}
}