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 Nested Navigation Flow
Semua orang tahu kalau foto akan terlihat baik dengan menggunakan filter. Pada artikel ini kita akan membuat aplikasi scrollable filter carousel dengan menggunakan foto dan filter yang sudah ada (menggunakan filter pada properti color
dan colorBlendMode
dari widget Image
. Contohnya aplikasinya sebagai berikut:
Berikut langkah-langkah untuk membuatnya:
1. Tambahkan Selector Ring dan Gradien Gelap
Nantinya filter yang terpilih akan ditampilkan dengan Selector Ring, selain itu kita akan menggunakan gradien gelap pada filter yang tersedia untuk membedakan antara filter dan foto yang sedang kita edit. Selanjutnya buat stateful widget baru yang disebut FilterSelector
untuk tempat implementasi selector.
@immutable class FilterSelector extends StatefulWidget { const FilterSelector({ Key? key, }) : super(key: key); @override _FilterSelectorState createState() => _FilterSelectorState(); } class _FilterSelectorState extends State<FilterSelector> { @override Widget build(BuildContext context) { return SizedBox(); } }
Tambahkan widget FilterSelector
ke widget tree yang ada dan posisikan widget FilterSelector
tepat berada di bagian bawah tengah foto yang dapat terlihat.
Stack( children: [ Positioned.fill( child: _buildPhotoWithFilter(), ), Positioned( left: 0.0, right: 0.0, bottom: 0.0, child: FilterSelector(), ), ], ),
Selanjutnya dengan menggunakan widget FilterSelector
, kita tampilkan selector ring di bagian atas gradien gelap menggunakan widget Stack
.
class _FilterSelectorState extends State<FilterSelector> { static const _filtersPerScreen = 5; static const _viewportFractionPerItem = 1.0 / _filtersPerScreen; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final itemSize = constraints.maxWidth * _viewportFractionPerItem; return Stack( alignment: Alignment.bottomCenter, children: [ _buildShadowGradient(itemSize), _buildSelectionRing(itemSize), ], ); }, ); } Widget _buildShadowGradient(double itemSize) { return SizedBox( height: itemSize * 2 + widget.padding.vertical, child: const DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black, ], ), ), child: SizedBox.expand(), ), ); } Widget _buildSelectionRing(double itemSize) { return IgnorePointer( child: Padding( padding: widget.padding, child: SizedBox( width: itemSize, height: itemSize, child: const DecoratedBox( decoration: BoxDecoration( shape: BoxShape.circle, border: Border.fromBorderSide( BorderSide(width: 6.0, color: Colors.white), ), ), ), ), ), ); } }
Ukuran selector cicrle dan gradien background tergantung dari ukuran individual filter pada carousal yang disebut itemSize
(tergantung kepada width yang tersedia). Oleh karena itu, widget LayoutBuilder
digunakan untuk menentukan available space dan kemudian kita dapat kalkuklasikan ukuran dari itemSize
individual filter yang ada. Selector ring juga menyertakan widget IgnorePointer
karena saat interaktivitas carousal ditambahkan, selector ring tidak boleh mengganggu tap dan drag event.
2. Membuat Item Filter Carousel
Setiap item filter pada carousel ditampilkan berupa circular image dengan jenis-jenis warna yang akan sesuai jika filter tersebut diterapkan pada foto. Selanjutnya, definisikan stateless widget baru yang disebut FilterItem
yang fungsinya untuk menampilkan single list item pada layar.
@immutable class FilterItem extends StatelessWidget { FilterItem({ Key? key, required this.color, this.onFilterSelected, }) : super(key: key); final Color color; final VoidCallback? onFilterSelected; @override Widget build(BuildContext context) { return GestureDetector( onTap: onFilterSelected, child: AspectRatio( aspectRatio: 1.0, child: Padding( padding: const EdgeInsets.all(8.0), child: ClipOval( child: Image.network( 'https://flutter.dev/docs/cookbook/img-files' '/effects/instagram-buttons/millenial-texture.jpg', color: color.withOpacity(0.5), colorBlendMode: BlendMode.hardLight, ), ), ), ), ); } }
3. Menerapkan filter Carousel
Item filter nantinya akan dapat di scroll ke kiri dan kanan dan scrolling ini membutuhkan semacam widget Scrollable
, dengan menggunakan widget PageView
kita akan membuat layout turunannya dari tengah space yang tersedia dan menyediakan snapping physics yang menyebabkan item langsung menuju ke tengah ketika pengguna melepaskan drag-nya. Selanjutnya konfigurasi widget tree agar memberi space untuk PageView
.
@override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { final itemSize = constraints.maxWidth * _viewportFractionPerItem; return Stack( alignment: Alignment.bottomCenter, children: [ _buildShadowGradient(itemSize), _buildCarousel(itemSize), _buildSelectionRing(itemSize), ], ); }); } Widget _buildCarousel(double itemSize) { return Container( height: itemSize, margin: widget.padding, child: PageView.builder( itemCount: widget.filters.length, itemBuilder: (context, index) { return SizedBox(); }, ), ); }
Kemudian buat setiap widget FilterItem
di dalam widget PageView
berdasarkan index
yang tersedia.
Color itemColor(int index) => widget.filters[index % widget.filters.length]; Widget _buildCarousel(double itemSize) { return Container( height: itemSize, margin: widget.padding, child: PageView.builder( itemCount: widget.filters.length, itemBuilder: (context, index) { return Center( child: FilterItem( color: itemColor(index), onFilterSelected: () {}, ), ); }, ), ); }
Widget PageView
menampilkan semua widgets FilterItem
dan kita dapat drag dari kiri dan kanan. Namun saat ini setiap widget FilterItem
memenuhi seluruh lebar layar dan setiap widget FilterItem
ditampilkan dengan ukuran dan opasitas yang sama. Kemudian nantinya ada 5 widget FilterItem
yang ditampilkan di layar, dan widget tersebut akan menyusut dan memudar saat bergerak ke ujung kiri atau kanan.
Solusi untuk kedua masalah di atas adalah dengan menerapkan PageViewController
. Properti PageViewController
’s viewportFraction
digunakan untuk menampilkan beberapa widget FilterItem
di layar secara bersamaan. Membangun kembali setiap widget FilterItem
saat PageViewController
berubah, memungkinkan kita untuk mengubah setiap ukuran dan opasitas widget FilterItem
saat pengguna melakukan scrolls.
Buat PageViewController
dan hubungkan ke widget PageView
.
class _FilterSelectorState extends State<FilterSelector> { late final PageController _controller; @override void initState() { super.initState(); _controller = PageController( viewportFraction: _viewportFractionPerItem, ); _controller.addListener(_onPageChanged); } void _onPageChanged() { final page = (_controller.page ?? 0).round(); widget.onFilterChanged(widget.filters[page]); } @override void dispose() { _controller.dispose(); super.dispose(); } Widget _buildCarousel(double itemSize) { return Container( height: itemSize, margin: widget.padding, child: PageView.builder( controller: _controller, itemCount: widget.filters.length, itemBuilder: (context, index) { return Center( child: FilterItem( color: itemColor(index), onFilterSelected: () {}, ), ); }, ), ); } }
Dengan menambahkan PageViewController
, 5 widget FilterItem
akan terlihat dilayar secara bersamaan dan foto akan berubah saat kita melakukan scroll ke kiri atau ke kanan. Namun, widget FilterItem
masih pada ukuran yang sama.
Selanjutnya bungkus setiap widegt FilterItem
dengan AnimatedBuilder
untuk mengubah properti visual dari setiap widget FilterItem
saat posisi scroll berubah.
Widget _buildCarousel(double itemSize) { return Container( height: itemSize, margin: widget.padding, child: PageView.builder( controller: _controller, itemCount: widget.filters.length, itemBuilder: (context, index) { return Center( child: AnimatedBuilder( animation: _controller, builder: (context, child) { return FilterItem( color: itemColor(index), onFilterSelected: () => {}, ); }, ), ); }, ), ); }
Widget AnimatedBuilder
me-rebuild setiap kali _controller
mengubah posisi scroll, sehingga memungkinkan kita untuk mengubah ukuran dan opasitas FilterItem
saat pengguna men-drags item filter.
Selanjutnya hitung skala dan opasitas yang sesuai untuk setiap widget FilterItem
di dalam AnimatedBuilder
dan terapkan nilai tersebut.
Widget _buildCarousel(double itemSize) { return Container( height: itemSize, margin: widget.padding, child: PageView.builder( controller: _controller, itemCount: widget.filters.length, itemBuilder: (context, index) { return Center( child: AnimatedBuilder( animation: _controller, builder: (context, child) { if (!_controller.hasClients || !_controller.position.hasContentDimensions) { // The PageViewController isn’t connected to the // PageView widget yet. Return an empty box. return SizedBox(); } // The integer index of the current page, // 0, 1, 2, 3, and so on final selectedIndex = _controller.page!.roundToDouble(); // The fractional amount that the current filter // is dragged to the left or right, for example, 0.25 when // the current filter is dragged 25% to the left. final pageScrollAmount = _controller.page! - selectedIndex; // The page-distance of a filter just before it // moves off-screen. final maxScrollDistance = _filtersPerScreen / 2; // The page-distance of this filter item from the // currently selected filter item. final pageDistanceFromSelected = (selectedIndex - index + pageScrollAmount).abs(); // The distance of this filter item from the // center of the carousel as a percentage, that is, where the selector // ring sits. final percentFromCenter = 1.0 - pageDistanceFromSelected / maxScrollDistance; final itemScale = 0.5 + (percentFromCenter * 0.5); final opacity = 0.25 + (percentFromCenter * 0.75); return Transform.scale( scale: itemScale, child: Opacity( opacity: opacity, child: FilterItem( color: itemColor(index), onFilterSelected: () => () {}, ), ), ); }, ), ); }, ), ); }
Saat ini, setiap widget FilterItem
akan menyusut dan hilang saat bergerak menjauhi tengah layar.
Kemudian tambahkan method untuk merubah filter yang dipilih saat widget FilterItem
diketuk atau di-tap.
void _onFilterTapped(int index) { _controller.animateToPage( index, duration: const Duration(milliseconds: 450), curve: Curves.ease, ); }
Konfigurasi setiap widget FilterItem
untuk menjalankan _onFilterTapped
saat diketuk.
FilterItem( color: itemColor(index), onFilterSelected: () => _onFilterTapped, )
Berikut cuplikan kode dan simulasinya, jika teman-teman menggunakan VSCode jalankan projectnya dengan menekan F5, klik hot reload (⚡) atau klik tombol ▶, berikut tampilannya:
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//