Membuat Carousel Yang Sempurna, Bagian 1
Indonesian (Bahasa Indonesia) translation by Andy Nur (you can also view the original English article)
Carousels adalah bahan pokok dari situs streaming dan e-commerce. Baik Amazon dan Netflix menggunakannya sebagai tool navigasi yang menonjol. Dalam tutorial ini, kita akan mengevaluasi desain interaksi keduanya, dan menggunakan temuan kita untuk menerapkan carousel yang sempurna.
Dalam rangkaian tutorial ini, kita juga akan mempelajari beberapa fungsi Popmotion, motion engine JavaScript. Ini menawarkan tool animasi seperti tweens (berguna untuk pagination), pelacakan pointer (untuk pengguliran), dan fisika pegas (untuk sentuhan akhir yang menyenangkan.)
Bagian 1 akan mengevaluasi bagaimana Amazon dan Netflix telah menerapkan pengguliran. Kita kemudian akan menerapkan carousel yang dapat digulirkan melalui sentuhan.
Pada akhir seri ini, kita akan menerapkan gulungan roda dan touchpad, pagination, progress bar, navigasi keyboard, dan beberapa sentuhan kecil yang menggunakan teknik fisika pegas. Kita juga akan memaparkan beberapa komposisi fungsional dasar.
Sempurna?
Apa yang dibutuhkan agar sebuah carousel menjadi "sempurna"? Ini harus dapat diakses oleh:
- Mouse: Ini harus menawarkan tombol sebelumnya dan berikutnya yang mudah diklik dan tidak mengaburkan konten.
- Touch: Harus melacak jari, lalu digulir dengan momentum yang sama seperti saat jari diangkat dari layar.
- Scroll wheel: Sering diabaikan, Apple Magic Mouse dan banyak trackpad laptop menawarkan pengguliran horizontal yang halus. Kita harus memanfaatkan kemampuan itu!
- Keyboard: Banyak pengguna memilih untuk tidak melakukannya, atau tidak dapat menggunakan mouse untuk navigasi. Penting agar carousel kita bisa diakses sehingga pengguna bisa menggunakan produk kita juga.
Akhirnya, kita akan melakukan hal-hal yang melangkah lebih jauh dan membuat ini menjadi bagian UX yang percaya diri dan menyenangkan dengan membuat carousel merespon dengan jelas dan secara mendalam dengan fisika pegas saat slider sudah mencapai akhir.
Pengaturan
Pertama, mari kita bahas HTML dan CSS yang diperlukan untuk membuat carousel yang belum sempurna dengan meng-fork CodePen ini.
Pen dibuat dengan Sass untuk preprocessing CSS dan Babel untuk mentransmisikan ES6 JavaScript. Saya juga menyertakan Popmotion, yang bisa diakses dengan window.popmotion
.
Kamu dapat menyalin kode ke proyek lokal jika kamu mau, namun kamu harus memastikan lingkungan kerjamu mendukung Sass dan ES6. Kamu juga perlu menginstal Popmotion dengan npm install popmotion
.
Membuat Carousel Baru
Pada halaman tertentu, kita mungkin memiliki banyak carousel. Jadi kita membutuhkan metode untuk merangkum state dan fungsinya masing-masing.
Saya akan menggunakan fungsi factory daripada sebuah class
. Fungsi factory menghindari kebutuhan untuk menggunakan kata this
yang sering membingungkan dan akan mempermudah kode untuk keperluan tutorial ini.
Di editor JavaScript-mu, tambahkan fungsi sederhana ini:
function carousel(container) { } carousel(document.querySelector('.container'));
Kita akan menambahkan kode spesifik-carousel kita di dalam fungsi carousel
ini.
Bagiamana dan Mengapa Pengguliran
Tugas pertama kita adalah membuat guliran carousel. Ada dua cara yang bisa kita lakukan:
Pengguliran Asli Browser
Solusi yang jelas adalah mengatur overflow-x: scroll
pada slider. Ini akan memungkinkan penggulir asli pada semua browser, termasuk sentuhan dan perangkat roda mouse horizontal.
Namun ada kelemahan untuk pendekatan ini:
- Konten di luar wadah tidak akan terlihat, yang bisa membatasi desain kita.
- Ini juga membatasi cara kita dapat menggunakan animasi untuk menunjukkan bahwa kita telah mencapai akhir.
- Browser desktop akan memiliki bilah gulir horizontal yang jelek (meski mudah diakses!).
Kalau tidak:
Animate translateX
Kita juga bisa menghidupkan properti translateX
carousel itu. Ini akan sangat serbaguna karena kita bisa menerapkan desain yang tepat yang kita sukai. translateX juga sangat bekerja, karena tidak seperti properti CSS left
yang itu dapat ditangani oleh perangkat GPU.
Pada sisi negatifnya, kita harus mengimplementasi ulang fungsi pengguliran menggunakan JavaScript. Itu lebih banyak pekerjaan, lebih banyak kode.
Bagaimana Pendekatan Pengguliran Amazon dan Netflix?
Baik carousel Amazon dan Netflix membuat penawaran yang berbeda dalam mendekati masalah ini.
Amazon menganimasi properti left
carousel saat berada dalam mode "desktop". Menganimasikan left
adalah pilihan yang sangat buruk, karena mengubahnya memicu perhitungan ulang tata letak. Ini adalah CPU-intensif, dan mesin yang lebih tua akan berjuang untuk mengenai 60fps.
Siapa pun yang membuat keputusan untuk menganimasi left
daripada translateX
pasti benar-benar idiot (spoiler: itu adalah saya, pada tahun 2012. Kami tidak begitu tercerahkan pada masa itu.)
Saat mendeteksi perangkat sentuh, carousel menggunakan pengguliran asli browser. Masalahnya hanya dengan mengaktifkan mode "mobile" ini adalah pengguna desktop dengan roda gulir horizontal yang tidak ada. Ini juga berarti konten di luar carousel harus diputar secara visual:



Netflix menganimasi dengan benar properti translateX
carousel itu, dan hal itu terjadi pada semua perangkat. Hal ini memungkinkan mereka memiliki desain yang melukai di luar carousel:



Hal ini, pada gilirannya, memungkinkan mereka untuk membuat desain yang mewah di mana item diperbesar di luar sudut x dan y dari carousel dan barang-barang disekitarnya bergerak keluar dari jalan mereka:



Sayangnya, implementasi ulang perangkat bergulir Netflix untuk perangkat sentuh tidak memuaskan: ia menggunakan sistem pagination berbasis gesture yang terasa lamban dan tidak praktis. Juga tidak ada pertimbangan untuk roda gulir horisontal.
Kita bisa berbuat lebih baik. Mari kita kode!
Pengguliran Seperti Profesional
Langkah pertama kita adalah meraih node .slider
. Sementara kita melakukannya, mari ambil item yang dikandungnya sehingga kita bisa mengetahui dimensi slider.
function carousel(container) { const slider = container.querySelector('.slider'); const items = slider.querySelectorAll('.item'); }
Mengukur Carousel
Kita bisa mengetahui area yang terlihat dari slider dengan mengukur lebarnya:
const sliderVisibleWidth = slider.offsetWidth;
Kita juga menginginkan total lebar semua item yang ada di dalamnya. Agar fungsi carousel
kita tetap relatif rapi, mari kita letakkan perhitungan ini di fungsi terpisah di bagian atas file kita.
Dengan menggunakan getBoundingClientRect
untuk mengukur offset left
item pertama kita dan offset right
item terakhir kita, kita dapat menggunakan perbedaan di antara keduanya untuk menemukan lebar total semua item.
function getTotalItemsWidth(items) { const { left } = items[0].getBoundingClientRect(); const { right } = items[items.length - 1].getBoundingClientRect(); return right - left; }
Setelah pengukuran sliderVisibleWidth
kita, tulis:
const totalItemsWidth = getTotalItemsWidth(items);
Kita sekarang bisa mengetahui jarak maksimum carousel kita yang boleh digulir. Ini adalah total lebar semua item kita, minus satu lebar penuh dari slider yang kita lihat. Ini menyediakan nomor yang memungkinkan item paling kanan untuk disesuaikan dengan benar pada slider kita:
const maxXOffset = 0; const minXOffset = - (totalItemsWidth - sliderVisibleWidth);
Dengan pengukuran ini, kita siap untuk mulai menggulir carousel kita.
Pengaturan translateX
Popmotion hadir dengan perender CSS untuk pengaturan CSS dan layanan yang sederhana. Ini juga dilengkapi dengan fungsi nilai yang dapat digunakan untuk melacak angka dan, yang penting (seperti yang akan kita lihat), untuk query kecepatannya.
Di bagian atas file JavaScript-mu, impor mereka seperti:
const { css, value } = window.popmotion;
Kemudian, pada baris setelah kita menetapkan minXOffset
, buatlah perender CSS untuk slider kita:
const sliderRenderer = css(slider);
Dan buat value
untuk melacak slider kita x offset dan perbarui properti translateX
slider saat ini berubah:
const sliderX = value(0, (x) => sliderRenderer.set('x', x));
Sekarang, pindahkan slider secara horizontal semudah menulis:
sliderX.set(-100);
Cobalah!
Sentuh Gulir
Kami ingin carouse kita mulai bergulir saat pengguna menyeret slider secara horizontal dan berhenti bergulir saat pengguna berhenti menyentuh layar. Event handler kita akan terlihat seperti ini:
let action; function stopTouchScroll() { document.removeEventListener('touchend', stopTouchScroll); } function startTouchScroll(e) { document.addEventListener('touchend', stopTouchScroll); } slider.addEventListener('touchstart', startTouchScroll, { passive: false });
Dalam fungsi startTouchScroll
kita, kita ingin:
- Menghentikan aksi lain yang menggerakkan
sliderX
. - Temukan sumber titik sentuhan.
- Perhatikan event
touchmove
berikutnya untuk melihat apakah pengguna menyeret secara vertikal atau horizontal.
Setelah document.addEventListener
, tambahkan:
if (action) action.stop();
Ini akan menghentikan aksi lain (seperti gulir momentum bertenaga fisika yang akan kita implementasikan di stopTouchScroll
) agar tidak memindahkan slider. Ini akan memungkinkan pengguna untuk segera "menangkap" slider jika menggulir melewati item atau judul yang ingin mereka klik.
Selanjutnya, kita perlu menyimpan titik sentuhan asal. Itu akan memungkinkan kita untuk melihat di mana pengguna menggerakkan jari mereka berikutnya. Jika itu gerakan vertikal, kita akan mengizinkan penggulir halaman seperti biasa. Jika itu adalah gerakan horizontal, kita akan menggulir slider sebagai gantinya.
Kita ingin membagi touchOrigin
ini di antara event handler. Jadi setelah let action;
tambahkan:
let touchOrigin = {};
Kembali ke handler startTouchScroll
kita, tambahkan:
const touch = e.touches[0]; touchOrigin = { x: touch.pageX, y: touch.pageY };
Kita sekarang dapat menambahkan event listener touchmove
ke document
untuk menentukan arah seret berdasarkan touchOrigin
ini:
document.addEventListener('touchmove', determineDragDirection);
Fungsi determineDragDirection
kita akan mengukur lokasi sentuhan berikutnya, memeriksanya apa benar-benar bergerak dan, jika demikian, ukur sudutnya untuk menentukan apakah vertikal atau horizontal:
function determineDragDirection(e) { const touch = e.changedTouches[0]; const touchLocation = { x: touch.pageX, y: touch.pageY }; }
Popmotion mencakup beberapa kalkulator yang membantu untuk mengukur hal-hal seperti jarak antara dua koordinat x/y. Kita bisa mengimpornya seperti ini:
const { calc, css, value } = window.popmotion;
Kemudian mengukur jarak antara kedua titik tersebut adalah masalah menggunakan kalkulator distance
:
const distance = calc.distance(touchOrigin, touchLocation);
Nah jika sentuhan sudah dipindah, kita bisa tidak mengeset event listener ini.
if (!distance) return; document.removeEventListener('touchmove', determineDragDirection);
Ukur sudut antara dua titik dengan kalkulator angle
:
const angle = calc.angle(touchOrigin, touchLocation);
Kita bisa menggunakan ini untuk menentukan apakah sudut ini sudut horizontal atau vertikal, dengan meneruskannya ke fungsi berikut. Tambahkan fungsi ini ke bagian paling atas dari file kita:
function angleIsVertical(angle) { const isUp = ( angle <= -90 + 45 && angle >= -90 - 45 ); const isDown = ( angle <= 90 + 45 && angle >= 90 - 45 ); return (isUp || isDown); }
Fungsi ini mengembalikan nilai true
jika sudut yang diberikan berada di dalam -90 +/- 45 derajat (lurus ke atas) atau 90 +/- 45 derajat (lurus ke bawah.) Jadi, kita dapat menambahkan ketentuan return
yang lain jika fungsi ini mengembalikan nilai true
.
if (angleIsVertical(angle)) return;
Pelacakan Pointer
Sekarang kita tahu pengguna sedang mencoba menggulir carousel, kita bisa mulai melacak jarinya. Popmotion menawarkan aksi pointer yang akan menampilkan koordinat x/y dari mouse atau pointer sentuh.
Pertama, impor pointer
:
const { calc, css, pointer, value } = window.popmotion;
Untuk melacak inputan sentuh, berikan event yang berasal ke pointer
:
action = pointer(e).start();
Kita ingin mengukur posisi awal x
dari pointer kita dan menerapkan gerakan apapun ke slider. Untuk itu, kita dapat menggunakan transformer yang disebut applyOffset
.
Transformer adalah fungsi murni yang mengambil nilai, dan mengembalikannya—ya—berubah. Misalnya: const double = (v) => v * 2
.
const { calc, css, pointer, transform, value } = window.popmotion; const { applyOffset } = transform;
applyOffset
adalah fungsi curry. Maksudnya bahwa ketika kita menyebutnya, itu menciptakan fungsi baru yang kemudian bisa diberi nilai. Kita pertama kali menyebutnya dengan nomor yang ingin kita ukur offset darinya, dalam hal ini yaitu nilai action.x
saat ini, dan sebuah angka untuk menerapkan offset itu. Dalam kasus ini, itu adalah sliderX
kita.
Jadi fungsi applyOffset
kita akan terlihat seperti ini:
const applyPointerMovement = applyOffset(action.x.get(), sliderX.get());
Kita sekarang dapat menggunakan fungsi ini pada callback output
pointer untuk menerapkan gerakan pointer ke slider.
action.output(({ x }) => slider.set(applyPointerMovement(x)));
Berhenti, Dengan Style
Carousel sekarang dapat diseret dengan sentuhan! Kamu dapat mengujinya dengan menggunakan emulasi perangkat di Chrome's Developer Tools.
Rasanya sedikit mengesalkan, kan? Kamu mungkin pernah mengalami pengguliran yang terasa seperti ini sebelumnya: Kamu mengangkat jarimu, dan pengguliran berhenti mati. Atau pengguliran berhenti mati dan kemudian animasi kecil mengambil alih untuk menyamarkan kelanjutan dari pengguliran tersebut.
Kami tidak akan melakukan itu. Kita bisa menggunakan aksi fisika di Popmotion untuk mengambil kecepatan sebenarnya dari sliderX
dan menerapkan pergeseran ke dalamnya selama durasi waktu.
Pertama, tambahkan ini ke daftar impor kita yang terus berkembang:
const { calc, css, physics, pointer, value } = window.popmotion;
Kemudian, di akhir fungsi stopTouchScroll
kita, tambahkan:
if (action) action.stop(); action = physics({ from: sliderX.get(), velocity: sliderX.getVelocity(), friction: 0.2 }) .output((v) => sliderX.set(v)) .start();
Di sini, from
dan velocity
diatur dengan nilai sekarang dan kecepatan sliderX
. Hal ini memastikan simulasi fisika kita memiliki kondisi awal yang sama dengan gerakan menyeret pada pengguna.
friction
ditetapkan sebagai 0.2
. Pergeseran diatur sebagai nilai dari 0
sampai 1
, dengan 0
tidak ada pergeseran sama sekali dan 1
merupakan pergeseran absolut. Cobalah bermain-main dengan nilai ini untuk melihat perubahan yang terjadi pada "perasaan" carousel saat pengguna berhenti menyeret.
Jumlah yang lebih kecil akan membuatnya terasa lebih ringan, dan jumlah yang lebih besar akan membuat gerakan lebih berat. Untuk gerakan bergulir, saya rasa 0.2
menyentuh keseimbangan yang bagus antara tidak menentu dan lamban.
Batas
Tapi ada masalah! Jika kamu telah bermain-main dengan carousel sentuh barumu, sudah jelas. Kita tidak membatasi gerakan, sehingga memungkinkan untuk benar-benar membuang carousel-mu pergi!
Ada transformator lain untuk pekerjaan ini, clamp
. Ini juga merupakan fungsi curry, artinya jika kita menyebutnya dengan nilai min dan max, katakanlah 0
dan 1
, itu akan mengembalikan fungsi baru. Dalam contoh ini, fungsi baru akan membatasi jumlah yang diberikan padanya antara 0
dan 1
:
clamp(0, 1)(5); // returns 1
Pertama, impor clamp
:
const { applyOffset, clamp } = transform;
Kita ingin menggunakan fungsi penjempit ini di seluruh carousel kita, jadi tambahkan baris ini setelah kita mendefinisikan minXOffset
:
const clampXOffset = clamp(minXOffset, maxXOffset);
Kita akan mengubah dua output
yang telah kita tetapkan pada aksi kita dengan menggunakan beberapa komposisi fungsional ringan dengan transformer pipe
.
Pipe
Saat kita memanggil fungsi, kita menuliskannya seperti ini:
foo(0);
Jika kita ingin memberikan output dari fungsi itu ke fungsi lain, kita mungkin menuliskannya seperti ini:
bar(foo(0));
Ini menjadi sedikit sulit untuk dibaca, dan itu semakin memburuk saat kita menambahkan lebih banyak fungsi.
Dengan pipe
, kita bisa membuat fungsi baru dari foo
dan bar
yang bisa kita gunakan kembali:
const foobar = pipe(foo, bar); foobar(0);
Ini juga ditulis dalam awal normal -> urutan akhir, yang membuatnya lebih mudah diikuti. Kita bisa menggunakan ini untuk menyusun applyOffset
dan clamp
menjadi satu fungsi. Impor pipe
:
const { applyOffset, clamp, pipe } = transform;
Ganti callback output
dari pointer
kita dengan:
pipe( ({ x }) => x, applyOffset(action.x.get(), sliderX.get()), clampXOffset, (v) => sliderX.set(v) )
Dan ganti callback output
dari physics
dengan:
pipe(clampXOffset, (v) => sliderX.set(v))
Komposisi fungsional semacam ini bisa cukup rapi membuatnya deskriptif, proses langkah-demi-langkah lebih kecil, fungsi yang dapat digunakan kembali.
Sekarang, ketika kamu menyeret dan melempar carousel, tidak akan bergerak keluar dari batas-batasnya.
Tiba-tiba berhenti tidak terlalu memuaskan. Tapi itu masalah untuk nanti!
Kesimpulan
Itu semua untuk bagian 1. Sejauh ini, kita telah melihat-lihat carousel yang ada untuk melihat kekuatan dan kelemahan dari berbagai pendekatan untuk pengguliran. Kita telah menggunakan pelacakan inputan dan fisika Popmotion untuk menampilkan animasi translateX
carousel kita dengan sentuhan bergulir. Kita juga telah diperkenalkan pada komposisi fungsional dan fungsi curry.
Kamu bisa mengambil versi komentar dari "cerita sejauh ini" pada CodePen ini.
Dalam angsuran mendatang, kita akan melihat:
- pengguliran dengan roda mouse
- mengukur ulang carousel saat window berubah ukuran
- paginasi, dengan aksesibilitas keyboard dan mouse
- sentuhan menyenangkan, dengan bantuan fisika musim semi
Berharap untuk melihatmu di sana!
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weekly