Untuk teman-teman yang akan baru memulai belajar Flutter silahkan masuk ke artikel ini dulu ya teman-teman ➡ Aplikasi Pertamaku “Halo Semuaaa…“. Jika sudah yuk lanjut baca artikel ini…
Jangan lupa baca artikel sebelumnya ya teman-teman ➡ Membuat Filter Foto Carousel
Ketika kita melakukan scroll sebuah list cards (contoh: di dalamnya terdapat gambar) pada suatu aplikasi, mungkin kita akan memperhatikan bahwa gambar-gambar tersebut tampak bergulir lebih lambat daripada bagian layar lainnya. Hampir terlihat seolah-olah cards pada list berada di latar depan dan gambar yang ada di cards seolah-olah berada jauh di background list (Efek ini dikenal sebagai parallax).
Maka pada artikel ini kita akan menerapkan efek parallax pada sebuah list cards (dengan sudut membulat berisi beberapa baris teks). Setiap cards juga berisi gambar yang dimana ketika cards di-slide ke arah atas, gambar yang berada di dalam cards akan bergeser ke bawah. Contohnya sebagai berikut:
Berikut langkah-langkah untuk membuatnya:
1. Membuat List untuk Menahan Parallax Items
Untuk menampilkan list dari gambar yang memiliki efek parallax, yang pertama kali harus kita lakukan yaitu menampilkan list.
Buat widget stateless baru bernama ParallaxRecipe
dan di dalam ParallaxRecipe
kita buat widget tree dengan SingleChildScrollView
dan Column
yang membentuk list.
class ParallaxRecipe extends StatelessWidget { @override Widget build(BuildContext context) { return SingleChildScrollView( child: Column( children: [], ), ); } }
2. Menampilkan Item dengan Teks dan Static Image
Setiap list item ditampilkan berbentuk persegi panjang sudut membulat dengan background sebuah gambar dan gambar tersebut berisi 7 lokasi terkenal di dunia. Kemudian di atas gambar tersebut terdapat nama lokasi dan negaranya yang diposisikan pada kiri bawah. Di antara gambar background dan teks terdapat gradien gelap yang akan mempermudah pengguna untuk membaca teks lokasi dan negara penjelas gambar background.
Implementasikan widget stateless yang diberi nama LocationListItem
yang terdiri dari visual yang sudah kita bahas sebelumnya. Untuk saat ini kita akan menggunakan widget Image
statik untuk background-nya, pada langkah selanjutnya baru kita ganti widget tersebut dengan versi parallax.
@immutable class LocationListItem extends StatelessWidget { const LocationListItem({ Key? key, required this.imageUrl, required this.name, required this.country, }) : super(key: key); final String imageUrl; final String name; final String country; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), child: AspectRatio( aspectRatio: 16 / 9, child: ClipRRect( borderRadius: BorderRadius.circular(16), child: Stack( children: [ _buildParallaxBackground(context), _buildGradient(), _buildTitleAndSubtitle(), ], ), ), ), ); } Widget _buildParallaxBackground(BuildContext context) { return Image.network( imageUrl, fit: BoxFit.cover, ); } Widget _buildGradient() { return Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( colors: [Colors.transparent, Colors.black.withOpacity(0.7)], begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: [0.6, 0.95], ), ), ), ); } Widget _buildTitleAndSubtitle() { return Positioned( left: 20, bottom: 20, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), Text( country, style: const TextStyle( color: Colors.white, fontSize: 14, ), ), ], ), ); } }
Lalu tambahkan list items pada widget ParallaxRecipe
.
class ParallaxRecipe extends StatelessWidget { @override Widget build(BuildContext context) { return SingleChildScrollView( child: Column( children: [ for (final location in locations) LocationListItem( imageUrl: location.imageUrl, name: location.name, location: location.place, ), ], ), ); } }
3. Menerapkan Parallax Effect
Ketika efek parallax scrolling berjalan, efek tersebut akan sedikit mendorong gambar background ke arah yang berlawanan dari list yang ada. Saat item daftar bergeser ke atas layar, setiap gambar background bergeser sedikit ke bawah. Sebaliknya, saat item daftar bergeser ke bawah layar, setiap gambar background bergeser sedikit ke atas. Secara visual, ini menghasilkan parallax efect.
Efek paralaks bergantung pada posisi list items terkini pada Scrollable
parrent-nya. Saat posisi scroll list items berubah, posisi gambar background list item juga harus berubah. Ini merupakan tantangan yang menarik untuk dipecahkan. Posisi list items dalam Scrollable
tidak tersedia sampai fase Flutter layout selesai. Artinya, posisi gambar latar ditentukan oleh fase paint, yang terjadi setelah fase layout. Untungnya, Flutter menyediakan widget bernama Flow
yang dirancang khusus untuk memberi kita kontrol atas transformasi widget child tepat sebelum widget di-paint. Dengan kata lain, kita dapat menghentikan fase painting dan mengambil kendali untuk mengubah posisi widget child sesuka kita.
Bungkus background widget Image
dengan widget Flow
.
Widget _buildParallaxBackground(BuildContext context) { return Flow( children: [ Image.network( imageUrl, fit: BoxFit.cover, ), ], ); }
Definisikan FlowDelegate
baru yang disebut ParallaxFlowDelegate
.
Widget _buildParallaxBackground(BuildContext context) { return Flow( delegate: ParallaxFlowDelegate(), children: [ Image.network( imageUrl, fit: BoxFit.cover, ), ], ); } // … class ParallaxFlowDelegate extends FlowDelegate { ParallaxFlowDelegate(); @override BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) { // TODO: } @override void paintChildren(FlowPaintingContext context) { // TODO: } @override bool shouldRepaint(covariant FlowDelegate oldDelegate) { // TODO: return true; } }
FlowDelegate
mengontrol bagaimana children diukur dan di mana children dicat. Dalam hal ini, widget Flow
hanya memiliki satu child yaitu gambar background. Gambar tersebut harus memiliki lebar yang sama dengan widget Flow
.
Return Constraints lebar untuk child gambar background.
@override BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) { return BoxConstraints.tightFor( width: constraints.maxWidth, ); }
Gambar background sekarang memiliki ukuran yang tepat, tapi kita tetap perlu memperhitungkan posisi vertikal setiap gambar background berdasarkan posisi scroll-nya kemudian mengecatnya.
Ada tiga informasi penting yang kita butuhkan untuk menghitung posisi gambar background yang diinginkan:
- Batasan yang ada di parent
Scrollable
- Batasan individu antara list item
- Ukuran gambar setelah diperkecil agar sesuai dengan list item
Untuk meliat batasan dari Scrollable
, kita harus meneruskan ScrollableState
ke FlowDelegate
.
Untuk melihat batasan individu antara list item, kita harus meneruskan list item’s BuildContext
keFlowDelegate
.
Untuk melihat ukuran akhir gambar background, kita harus menetapkan sebuah GlobalKey
pada widget Image
dan kemudian kita meneruskan GlobalKey
tersebut ke FlowDelegate
.
class LocationListItem extends StatelessWidget { final GlobalKey _backgroundImageKey = GlobalKey(); Widget _buildParallaxBackground(BuildContext context) { return Flow( delegate: ParallaxFlowDelegate( scrollable: Scrollable.of(context)!, listItemContext: context, backgroundImageKey: _backgroundImageKey, ), children: [ Image.network( imageUrl, key: _backgroundImageKey, fit: BoxFit.cover, ), ], ); } } // … class ParallaxFlowDelegate extends FlowDelegate { ParallaxFlowDelegate({ required this.scrollable, required this.listItemContext, required this.backgroundImageKey, }); final ScrollableState scrollable; final BuildContext listItemContext; final GlobalKey backgroundImageKey; }
Setelah kita memiliki semua informasi yang diperlukan untuk mengimplementasikan parallax scrolling, selanjutnya kita implementasikan method shouldRepaint()
.
@override bool shouldRepaint(ParallaxFlowDelegate oldDelegate) { return scrollable != oldDelegate.scrollable || listItemContext != oldDelegate.listItemContext || backgroundImageKey != oldDelegate.backgroundImageKey; }
Sekarang terapkan perhitungan layout untuk efek parallax. Pertama, hitung posisi pixel dari list item di dalam parent Scrollable
.
@override void paintChildren(FlowPaintingContext context) { // Calculate the position of this list item within the viewport. final scrollableBox = scrollable.context.findRenderObject() as RenderBox; final listItemBox = listItemContext.findRenderObject() as RenderBox; final listItemOffset = listItemBox.localToGlobal( listItemBox.size.centerLeft(Offset.zero), ancestor: scrollableBox); }
Selanjutnya gunakan posisi pixel dari list item untuk menghitung persentase dari atas Scrollable
. List Item di bagian atas pada area yang dapat di-scroll harus menghasilkan 0%, dan List Item di bagian bawah pada area yang dapat di-scroll harus menghasilkan 100%.
@override void paintChildren(FlowPaintingContext context) { // Calculate the position of this list item within the viewport. final scrollableBox = scrollable.context.findRenderObject() as RenderBox; final listItemBox = listItemContext.findRenderObject() as RenderBox; final listItemOffset = listItemBox.localToGlobal( listItemBox.size.centerLeft(Offset.zero), ancestor: scrollableBox); // Determine the percent position of this list item within the // scrollable area. final viewportDimension = scrollable.position.viewportDimension; final scrollFraction = (listItemOffset.dy / viewportDimension).clamp(0.0, 1.0); }
Lalu gunakan persentase scroll untuk menghitung Alignment
. Pada 0%, tetapkan Alignment(0.0, -1.0)
dan pada 100% tetapkan Alignment(0.0, 1.0)
dan setiap koordinat tersebut sesuai dengan Alignment
top dan bottom.
@override void paintChildren(FlowPaintingContext context) { // Calculate the position of this list item within the viewport. final scrollableBox = scrollable.context.findRenderObject() as RenderBox; final listItemBox = listItemContext.findRenderObject() as RenderBox; final listItemOffset = listItemBox.localToGlobal( listItemBox.size.centerLeft(Offset.zero), ancestor: scrollableBox); // Determine the percent position of this list item within the // scrollable area. final viewportDimension = scrollable.position.viewportDimension; final scrollFraction = (listItemOffset.dy / viewportDimension).clamp(0.0, 1.0); // Calculate the vertical alignment of the background // based on the scroll percentage. final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1); }
Gunakan verticalAlignment
bersamaan dengan ukuran list item dan ukuran dari gambar background, untuk menghasilkan Rect
yang menentukan dimana gambar background harus diposisikan.
@override void paintChildren(FlowPaintingContext context) { // Calculate the position of this list item within the viewport. final scrollableBox = scrollable.context.findRenderObject() as RenderBox; final listItemBox = listItemContext.findRenderObject() as RenderBox; final listItemOffset = listItemBox.localToGlobal( listItemBox.size.centerLeft(Offset.zero), ancestor: scrollableBox); // Determine the percent position of this list item within the // scrollable area. final viewportDimension = scrollable.position.viewportDimension; final scrollFraction = (listItemOffset.dy / viewportDimension).clamp(0.0, 1.0); // Calculate the vertical alignment of the background // based on the scroll percentage. final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1); // Convert the background alignment into a pixel offset for // painting purposes. final backgroundSize = (backgroundImageKey.currentContext!.findRenderObject() as RenderBox) .size; final listItemSize = context.size; final childRect = verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize); }
Gunakan childRect
untuk melukis gambar background dengan transformasi yang kita inginkan dan perubahan transformasi inilah yang akan memberikan efek parallax.
@override void paintChildren(FlowPaintingContext context) { // Calculate the position of this list item within the viewport. final scrollableBox = scrollable.context.findRenderObject() as RenderBox; final listItemBox = listItemContext.findRenderObject() as RenderBox; final listItemOffset = listItemBox.localToGlobal( listItemBox.size.centerLeft(Offset.zero), ancestor: scrollableBox); // Determine the percent position of this list item within the // scrollable area. final viewportDimension = scrollable.position.viewportDimension; final scrollFraction = (listItemOffset.dy / viewportDimension).clamp(0.0, 1.0); // Calculate the vertical alignment of the background // based on the scroll percentage. final verticalAlignment = Alignment(0.0, scrollFraction * 2 - 1); // Convert the background alignment into a pixel offset for // painting purposes. final backgroundSize = (backgroundImageKey.currentContext!.findRenderObject() as RenderBox) .size; final listItemSize = context.size; final childRect = verticalAlignment.inscribe(backgroundSize, Offset.zero & listItemSize); // Paint the background. context.paintChild( 0, transform: Transform.translate( offset: Offset(0.0, childRect.top), ).transform, ); }
Langkah akhir kita akan menambahkan detail pelengkap pada efek parallax, dimana ParallaxFlowDelegate
mengecat ulang saat masukan berubah tapi ParallaxFlowDelegate
tidak mengecat ulang setiap posisi scroll berubah. Oleh karena itu kita perlu meneruskan ScrollableState
’s ScrollPosition
ke superclass FlowDelegate
sehingga FlowDelegate
mengecat ulang setiap kali perubahan ScrollPosition
.
class ParallaxFlowDelegate extends FlowDelegate { ParallaxFlowDelegate({ required this.scrollable, required this.listItemContext, required this.backgroundImageKey, }) : super(repaint: scrollable.position); }
Berikut cuplikan kode dan simulasinya, jika teman-teman menggunakan VSCode jalankan projectnya dengan menekan F5, klik hot reload (⚡) atau klik tombol ▶, berikut tampilannya (lakukan scroll ke atas dan ke bawah):
Jika ada pertanyaan silahkan komen dan jika artikel ini dirasa bermanfaat, jangan lupa like dan sharenya ya teman-teman. ??????? Sampai bertemu di artikel selanjutnya.
Terima Kasih, Assalamu’alaykum… Salam KODINGINDONESIA
Referensi : https://flutter.dev//