Update page_intro.dart

This commit is contained in:
Greek-Cp 2025-03-23 01:02:13 +07:00
parent 77fc37ee52
commit 96d1c1ea3c
1 changed files with 511 additions and 218 deletions

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:harvest_guard_app/routes/app_routes.dart'; import 'package:harvest_guard_app/routes/app_routes.dart';
import 'dart:math' as math;
class IntroScreen extends StatefulWidget { class IntroScreen extends StatefulWidget {
const IntroScreen({super.key}); const IntroScreen({super.key});
@ -9,274 +10,566 @@ class IntroScreen extends StatefulWidget {
State<IntroScreen> createState() => _IntroScreenState(); State<IntroScreen> createState() => _IntroScreenState();
} }
class _IntroScreenState extends State<IntroScreen> { class _IntroScreenState extends State<IntroScreen>
final PageController _pageController = PageController(); with TickerProviderStateMixin {
// Controller untuk animasi
late final AnimationController _backgroundAnimController;
late final AnimationController _contentAnimController;
// Halaman saat ini
int _currentPage = 0; int _currentPage = 0;
final List<IntroPage> _introPages = [ // Data halaman intro
IntroPage( final List<IntroData> _introPages = [
IntroData(
title: 'Pilih Varietas Padi', title: 'Pilih Varietas Padi',
description: description:
'Pilih jenis padi yang ingin Anda periksa dari daftar varietas yang tersedia', 'Pilih jenis padi yang ingin Anda periksa dari berbagai varietas tanaman padi yang tersedia',
icon: Icons.grass_rounded, image: 'icons/ic_rice_plant.png',
color: Colors.green.shade400, bgColor: const Color(0xFF4CAF50),
overlayColor: const Color(0xFF2E7D32),
lightColor: const Color(0xFFA5D6A7),
), ),
IntroPage( IntroData(
title: 'Scan Tanaman Padi', title: 'Scan Tanaman Padi',
description: description:
'Arahkan kamera ponsel Anda ke bagian tanaman padi yang ingin diperiksa', 'Arahkan kamera ke bagian tanaman yang ingin diperiksa untuk analisis dan deteksi penyakit',
icon: Icons.document_scanner_rounded, image: 'icons/ic_camera.png',
color: Colors.blue.shade400, bgColor: const Color(0xFF2196F3),
overlayColor: const Color(0xFF1565C0),
lightColor: const Color(0xFF90CAF9),
), ),
IntroPage( IntroData(
title: 'Cek Hasil Diagnosis', title: 'Hasil Diagnosis',
description: description:
'Dapatkan hasil diagnosis penyakit dan rekomendasi penanganannya', 'Lihat hasil diagnosis penyakit beserta rekomendasi cara penanganan dan pencegahannya',
icon: Icons.checklist_rounded, image: 'icons/ic_result.png',
color: Colors.orange.shade400, bgColor: const Color(0xFFFF9800),
overlayColor: const Color(0xFFEF6C00),
lightColor: const Color(0xFFFFCC80),
), ),
]; ];
// PageController tunggal hanya untuk pergantian halaman
late final PageController _pageController;
@override
void initState() {
super.initState();
// Inisialisasi page controller
_pageController = PageController();
_pageController.addListener(_handlePageChange);
// Inisialisasi animasi controller
_backgroundAnimController = AnimationController(
vsync: this,
duration: const Duration(seconds: 20),
)..repeat();
_contentAnimController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
)..forward();
}
void _handlePageChange() {
// Jika halaman saat ini berbeda dari _currentPage
final page = (_pageController.page ?? 0).round();
if (page != _currentPage) {
setState(() {
_currentPage = page;
// Reset dan mulai animasi konten
_contentAnimController.reset();
_contentAnimController.forward();
});
}
}
@override @override
void dispose() { void dispose() {
_pageController.removeListener(_handlePageChange);
_pageController.dispose(); _pageController.dispose();
_backgroundAnimController.dispose();
_contentAnimController.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Size screenSize = MediaQuery.of(context).size;
return Scaffold( return Scaffold(
body: Stack( body: Stack(
children: [ children: [
// Background animasi bergerak // Background animasi
AnimatedPositioned( _buildAnimatedBackground(),
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
top: -100 + (_currentPage * 50),
right: -100 + (_currentPage * 40),
child: Container(
height: 300,
width: 300,
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [
_introPages[_currentPage].color.withOpacity(0.7),
_introPages[_currentPage].color.withOpacity(0.0),
],
stops: const [0.1, 1.0],
),
shape: BoxShape.circle,
),
),
),
AnimatedPositioned(
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
bottom: -80 + (_currentPage * 30),
left: -50 + (_currentPage * 20),
child: Container(
height: 250,
width: 250,
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [
_introPages[_currentPage].color.withOpacity(0.5),
_introPages[_currentPage].color.withOpacity(0.0),
],
stops: const [0.1, 1.0],
),
shape: BoxShape.circle,
),
),
),
// Content // Konten utama dengan PageView
Column( PageView.builder(
children: [ controller: _pageController,
Expanded( itemCount: _introPages.length,
child: PageView.builder( onPageChanged: (page) {
controller: _pageController, setState(() {
itemCount: _introPages.length, _currentPage = page;
onPageChanged: (int page) { // Reset animasi konten saat halaman berubah
setState(() { _contentAnimController.reset();
_currentPage = page; _contentAnimController.forward();
}); });
}, },
itemBuilder: (context, index) { itemBuilder: (context, index) {
return _buildIntroPage(_introPages[index]); return _buildFullPageContent(_introPages[index], screenSize);
}, },
),
),
// Pagination indicator
Container(
padding: const EdgeInsets.symmetric(vertical: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
_introPages.length,
(index) => _buildDotIndicator(index),
),
),
),
// Bottom buttons
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Next/Finish button
ElevatedButton(
onPressed: () {
if (_currentPage < _introPages.length - 1) {
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
} else {
Get.offNamed(AppRoutes.dashboard);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: _introPages[_currentPage].color,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 30, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: Text(
_currentPage < _introPages.length - 1
? 'Lanjut'
: 'Mulai',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
],
), ),
], ],
), ),
); );
} }
Widget _buildIntroPage(IntroPage page) { // Background dengan animasi
return Padding( Widget _buildAnimatedBackground() {
padding: const EdgeInsets.symmetric(horizontal: 30.0), return AnimatedBuilder(
child: Column( animation: _backgroundAnimController,
mainAxisAlignment: MainAxisAlignment.center, builder: (context, child) {
children: [ return Stack(
// Icon with animated container children: [
TweenAnimationBuilder<double>( // Base background color
tween: Tween(begin: 0.0, end: 1.0), AnimatedContainer(
duration: const Duration(milliseconds: 600), duration: const Duration(milliseconds: 500),
builder: (context, value, child) { color: _introPages[_currentPage].bgColor,
return Transform.scale( ),
scale: value,
child: Container( // Animated shapes - circles
padding: const EdgeInsets.all(25), Positioned(
right: -100,
top: -50,
child: Transform.rotate(
angle: _backgroundAnimController.value * 2 * math.pi,
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
width: 300,
height: 300,
decoration: BoxDecoration( decoration: BoxDecoration(
color: page.color.withOpacity(0.2), shape: BoxShape.circle,
color:
_introPages[_currentPage].lightColor.withOpacity(0.3),
),
),
),
),
// Animated shapes - blob
Positioned(
left: -150 +
(math.sin(_backgroundAnimController.value * math.pi) * 50),
bottom: 100 +
(math.cos(_backgroundAnimController.value * math.pi) * 50),
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
width: 350,
height: 350,
decoration: BoxDecoration(
shape: BoxShape.circle,
color:
_introPages[_currentPage].overlayColor.withOpacity(0.2),
),
),
),
// Small decorative elements
for (int i = 0; i < 5; i++)
Positioned(
top: (i * 100) +
(math.sin(
_backgroundAnimController.value * 2 * math.pi + i) *
20),
right: (i % 2 == 0) ? 40 + (i * 30) : null,
left: (i % 2 != 0) ? 40 + (i * 20) : null,
child: Opacity(
opacity: 0.3,
child: Container(
width: 15 + (i * 3),
height: 15 + (i * 3),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.6),
),
),
),
),
],
);
},
);
}
// Widget untuk konten halaman penuh
Widget _buildFullPageContent(IntroData data, Size screenSize) {
return Column(
children: [
// Bagian Header - Occupies 40% of screen
SizedBox(
height: screenSize.height * 0.45,
child: _buildHeaderContent(data),
),
// Bagian konten bawah
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, -5),
),
],
),
child: Column(
children: [
const SizedBox(height: 30),
// Indikator halaman
_buildPageIndicator(),
const SizedBox(height: 30),
// Konten deskripsi
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: _buildDescriptionContent(data),
),
),
// Tombol navigasi
_buildNavigationButtons(),
],
),
),
),
],
);
}
// Widget untuk bagian header
Widget _buildHeaderContent(IntroData data) {
return SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo atau branding
Padding(
padding: const EdgeInsets.only(top: 16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.eco_rounded,
color: Colors.white,
size: 24,
),
),
const SizedBox(width: 12),
const Text(
'Harvest Guard',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
const Spacer(),
// Ilustrasi tengah
AnimatedBuilder(
animation: _contentAnimController,
builder: (context, child) {
// Scale up animation
return Transform.scale(
scale: _contentAnimController.value,
child: Opacity(
opacity: _contentAnimController.value,
child: child,
),
);
},
child: Container(
width: 180,
height: 180,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Container(
page.icon, decoration: const BoxDecoration(
size: 80, color: Colors.white,
color: page.color, shape: BoxShape.circle,
),
),
);
},
),
const SizedBox(height: 40),
// Title
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 800),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 20 * (1 - value)),
child: Text(
page.title,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87,
), ),
textAlign: TextAlign.center, child: Icon(
), _getIconForImage(data.image),
), size: 60,
); color: data.bgColor,
},
),
const SizedBox(height: 20),
// Description
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 1000),
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Transform.translate(
offset: Offset(0, 30 * (1 - value)),
child: Text(
page.description,
style: const TextStyle(
fontSize: 16,
color: Colors.black54,
height: 1.5,
), ),
textAlign: TextAlign.center,
), ),
), ),
); ),
}, ),
const Spacer(),
],
),
),
);
}
// Widget untuk konten deskripsi
Widget _buildDescriptionContent(IntroData data) {
return AnimatedBuilder(
animation: _contentAnimController,
builder: (context, child) {
// Slide up and fade in animation
return Transform.translate(
offset: Offset(0, 50 * (1 - _contentAnimController.value)),
child: Opacity(
opacity: _contentAnimController.value,
child: child,
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
Text(
data.title,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: data.bgColor,
height: 1.2,
),
),
const SizedBox(height: 20),
// Description
Text(
data.description,
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade700,
height: 1.6,
),
),
const SizedBox(height: 20),
// Feature points - menambahkan beberapa fitur spesifik
...(_getFeaturesForPage(data))
.map((feature) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: data.bgColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
feature.icon,
size: 16,
color: data.bgColor,
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
feature.text,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
),
],
),
))
.toList(),
// Padding di bawah untuk memastikan scroll aman
const SizedBox(height: 20),
],
),
),
);
}
// Widget untuk indikator halaman
Widget _buildPageIndicator() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(_introPages.length, (index) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.symmetric(horizontal: 5),
height: 8,
width: _currentPage == index ? 30 : 8,
decoration: BoxDecoration(
color: _currentPage == index
? _introPages[_currentPage].bgColor
: Colors.grey.shade300,
borderRadius: BorderRadius.circular(4),
),
);
}),
);
}
// Widget untuk tombol navigasi
Widget _buildNavigationButtons() {
return Padding(
padding: const EdgeInsets.fromLTRB(30, 20, 30, 40),
child: Row(
children: [
const Spacer(),
// Next/Finish button
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: _currentPage < _introPages.length - 1 ? 60 : 160,
height: 60,
child: ElevatedButton(
onPressed: () {
if (_currentPage < _introPages.length - 1) {
_pageController.animateToPage(
_currentPage + 1,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
} else {
Get.offNamed(AppRoutes.dashboard);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: _introPages[_currentPage].bgColor,
foregroundColor: Colors.white,
elevation: 2,
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
child: _currentPage < _introPages.length - 1
? const Icon(Icons.arrow_forward_rounded, size: 24)
: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Mulai Sekarang',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 8),
Icon(Icons.arrow_forward_rounded, size: 20),
],
),
),
), ),
], ],
), ),
); );
} }
Widget _buildDotIndicator(int index) { // Helper untuk mendapatkan ikon berdasarkan nama file
return AnimatedContainer( IconData _getIconForImage(String imagePath) {
duration: const Duration(milliseconds: 300), if (imagePath.contains('rice_plant')) {
margin: const EdgeInsets.symmetric(horizontal: 5), return Icons.grass_rounded;
height: 8, } else if (imagePath.contains('camera')) {
width: _currentPage == index ? 24 : 8, return Icons.camera_alt_rounded;
decoration: BoxDecoration( } else if (imagePath.contains('result')) {
color: _currentPage == index return Icons.fact_check_rounded;
? _introPages[_currentPage].color }
: Colors.grey.shade300, return Icons.eco_rounded;
borderRadius: BorderRadius.circular(4), }
),
); // Helper untuk mendapatkan fitur spesifik berdasarkan halaman
List<FeatureItem> _getFeaturesForPage(IntroData data) {
if (data.title.contains('Pilih Varietas')) {
return [
FeatureItem(Icons.category_rounded, 'Berbagai varietas padi tersedia'),
FeatureItem(Icons.info_rounded, 'Informasi detail setiap varietas'),
FeatureItem(Icons.bookmark_rounded, 'Simpan favorit untuk akses cepat'),
];
} else if (data.title.contains('Scan')) {
return [
FeatureItem(Icons.autorenew_rounded, 'Deteksi cepat & akurat'),
FeatureItem(Icons.photo_library_rounded, 'Gunakan foto dari galeri'),
FeatureItem(Icons.crop_rounded, 'Crop gambar untuk hasil terbaik'),
];
} else {
return [
FeatureItem(Icons.timeline_rounded, 'Analisis detail penyakit'),
FeatureItem(Icons.healing_rounded, 'Rekomendasi penanganan'),
FeatureItem(Icons.history_rounded, 'Riwayat pemeriksaan'),
];
}
} }
} }
class IntroPage { // Model untuk data intro page
class IntroData {
final String title; final String title;
final String description; final String description;
final IconData icon; final String image;
final Color color; final Color bgColor;
final Color overlayColor;
final Color lightColor;
IntroPage({ IntroData({
required this.title, required this.title,
required this.description, required this.description,
required this.icon, required this.image,
required this.color, required this.bgColor,
required this.overlayColor,
required this.lightColor,
}); });
} }
// Model untuk item fitur
class FeatureItem {
final IconData icon;
final String text;
FeatureItem(this.icon, this.text);
}