Flutter Effects – Membuat Filter Foto Carousel

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-temanMembuat 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:

Photo Filter Carousel

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//

Anton Prafanto

Konten developer kodingindonesia.com & staf pengajar tetap di Universitas Mulawarman Samarinda

all author posts