Advertisement
  1. Web Design
  2. HTML/CSS
  3. Animation

Membuat Carousel Yang Sempurna, Bagian 3

Scroll to top
Read Time: 12 min
This post is part of a series called Create the Perfect Carousel.
Create the Perfect Carousel, Part 2

() translation by (you can also view the original English article)

Ini adalah bagian ketiga dan yang terakhir dari seri tutorial Membuat Carousel Yang Sempurna. Pada bagian 1, kita menilai carousel di Netflix dan Amazon, dua dari carousel yang paling banyak digunakan di dunia. Kita menyiapkan carousel kita dan mengimplementasikan gulir sentuhan.

Kemudian pada bagian 2, kita menambahkan gulir mouse horizontal, pagination, dan indikator perkembangan. Boom.

Sekarang, di bagian akhir kita, kita akan melihat ke dalam aksesibilitas dunia keyboard yang suram dan terlupakan. Kita akan menyesuaikan kode kita untuk mengukur kembali carousel saat ukuran viewport berubah. Dan akhirnya, kita akan memberi beberapa sentuhan akhir menggunakan teknik spring physics.

Kamu dapat mengambil dimana kita meninggalkannya dengan CodePen ini.

Aksesibilitas Keyboard

Memang benar mayoritas pengguna tidak mengandalkan navigasi keyboard, jadi sayangnya terkadang kita melupakan pengguna kita yang melakukannya. Di beberapa negara, membiarkan situs web tidak dapat diakses mungkin ilegal. Tetapi yang lebih buruk lagi, ini adalah tindakan detektif.

Kabar baiknya adalah bahwa hal itu biasanya mudah diterapkan! Sebenarnya, browser melakukan sebagian besar pekerjaan untuk kita. Serius: coba tabbing melalui carousel yang telah kita buat. Karena kita telah menggunakan markup semantik, Kamu sudah bisa!

Kecuali, kamu akan melihat, tombol navigasi kita hilang. Ini karena browser tidak mengizinkan fokus pada elemen di luar viewport kita. Jadi meskipun kita telah menetapkan overflow: hidden, kita tidak dapat menggulir halaman secara horizontal; Jika tidak, halaman pasti akan bergulir untuk menampilkan elemen dengan fokus.

Ini tidak apa-apa, dan itu akan memenuhi syarat, menurut pendapat saya, sebagai "hal yang berguna", meski tidak terlalu menyenangkan.

Carousel Netflix juga bekerja dengan cara ini. Tetapi karena sebagian besar judul mereka lamban dimuat, dan juga secara pasif keyboard mudah diakses (artinya mereka belum menulis kode apa pun secara khusus untuk menanganinya), kita sebenarnya tidak dapat memilih judul apa pun selain beberapa dari yang sudah kita muat. Ini juga terlihat mengerikan:

Keyboard AccessibilityKeyboard AccessibilityKeyboard Accessibility

Kita bisa berbuat lebih baik.

Tangani event focus

Untuk melakukan ini, kita akan mendengarkan event focus yang menyala pada item apapun di carousel tersebut. Saat sebuah item menerima fokus, kita akan menanyakannya untuk posisinya. Kemudian, kita akan memeriksa bahwa pada sliderX dan sliderVisibleWidth untuk melihat apakah item itu ada di dalam window yang terlihat. Jika tidak, kita akan membuat paginasi dengan menggunakan kode yang sama dengan yang kita tulis di bagian 2.

Di akhir fungsi carousel, tambahkan event listener ini:

1
slider.addEventListener('focus', onFocus, true);

Anda akan melihat bahwa kita telah memberikan parameter ketiga, true. Daripada menambahkan event listener ke setiap item, kita dapat menggunakan apa yang dikenal sebagai event delegation untuk mendengarkan event hanya pada satu elemen, induknya langsung. Event focus tidak bergelembung, jadi true yang mengatakan kepada event listener untuk mendengarkan tahap penangkapan, tahap dimana event terjadi pada setiap elemen dari window sampai ke target (dalam hal ini, item yang menerima fokus).

Di atas kumpulan event listener kita, tambahkan fungsi onFocus:

1
function onFocus(e) {
2
}

Kita akan bekerja dalam fungsi ini untuk sisa bagian ini.

Kita perlu mengukur offset left dan right item dan memeriksa apakah kedua titik berada di luar area yang saat ini dapat dilihat.

Item disediakan oleh parameter event target, dan kita dapat mengukurnya dengan getBoundingClientRect:

1
const { left, right } = e.target.getBoundingClientRect();

left dan right relatif terhadap viewport, bukan slider. Jadi kita perlu membuat kontainer carousel offset left untuk memperhitungkannya. Dalam contoh kita, ini akan menjadi 0, tapi untuk membuat carousel yang kuat, seharusnya akun ditempatkan di mana saja.

1
const carouselLeft = container.getBoundingClientRect().left;

Sekarang, kita dapat melakukan pemeriksaan sederhana untuk melihat apakah item berada di luar area yang terlihat oleh slider dan dipaginasi ke arah itu:

1
if (left < carouselLeft) {
2
  gotoPrev();
3
} else if (right > carouselLeft + sliderVisibleWidth) {
4
  gotoNext();
5
}

Sekarang, saat kita tab disekitar, carousel dengan percaya diri berkepentingan dengan fokus keyboard kita! Cukup beberapa baris kode untuk menunjukkan lebih banyak cinta kepada pengguna kita.

Mengukur Kembali Carousel

Kamu mungkin telah memperhatikan saat kamu mengikuti tutorial ini bahwa jika kamu mengubah ukuran viewport browser-mu, carousel tidak dipaginasi dengan benar lagi. Ini karena kita mengukur lebarnya relatif terhadap area yang terlihat sekali saja, pada saat inisialisasi.

Untuk memastikan carousel kita berperilaku benar, kita perlu mengganti beberapa kode pengukuran dengan event listener baru yang menyala saat windows diubah ukurannya.

Sekarang, menjelang awal fungsi carousel-mu, tepat setelah baris di mana kita mendefinisikan progressBar, kita ingin mengganti tiga dari pengukuran const ini dengan let, karena kita akan mengubahnya saat perubahan viewport:

1
const totalItemsWidth = getTotalItemsWidth(items);
2
const maxXOffset = 0;
3
4
let minXOffset = 0;
5
let sliderVisibleWidth = 0;
6
let clampXOffset;

Kemudian, kita bisa memindahkan logika yang sebelumnya menghitung nilai-nilai ini ke fungsi measureCarousel baru:

1
function measureCarousel() {
2
  sliderVisibleWidth = slider.offsetWidth;
3
  minXOffset = - (totalItemsWidth - sliderVisibleWidth);
4
  clampXOffset = clamp(minXOffset, maxXOffset);
5
}

Kita ingin segera memanggil fungsi ini sehingga kita tetap menetapkan nilai inisialisasinya. Pada baris berikutnya, panggil measureCarousel:

1
measureCarousel();

Carousel seharusnya bekerja persis seperti sebelumnya. Untuk memperbarui pada perubahan ukuran window, kita cukup menambahkan event listener ini di bagian akhir fungsi carousel kita:

1
window.addEventListener('resize', measureCarousel);

Sekarang, jika kamu mengubah ukuran carousel dan mencoba paginasi, itu akan terus bekerja seperti yang diharapkan.

Catatan Tentang Performa

Perlu dipertimbangkan bahwa di dunia nyata, kamu mungkin memiliki banyak carousel di halaman yang sama, mengalikan dampak kinerja dari kode pengukuran ini dengan jumlah tersebut.

Seperti yang telah kita bahas secara singkat di bagian 2, tidaklah bijaksana untuk melakukan perhitungan berat lebih sering dari yang kamu inginkan. Dengan event pointer dan scroll, kita mengatakan bahwa kamu ingin melakukan sekali per frame untuk membantu mempertahankan 60fps. Mengubah ukuran event sedikit berbeda karena seluruh dokumen akan berubah, mungkin momen sumber daya yang paling intensif akan ditemui pada halaman web.

Kita tidak perlu mengukur kembali carouse tersebut sampai pengguna selesai mengubah ukuran jendela, karena mereka tidak akan berinteraksi dengannya saat ini. Kita bisa membungkus fungsi measureCarousel kita dalam fungsi khusus yang disebut debounce.

Fungsi debounce pada dasarnya mengatakan: "Hanya nyalakan fungsi ini jika tidak dipanggil lebih dari x milidetik." Kamu dapat membaca lebih lanjut tentang debounce pada David Walsh's excellent primer, dan juga mengambil beberapa contoh kode.

Sentuhan Akhir

Sejauh ini, kita telah menciptakan sebuah korsel yang cukup bagus. Ini mudah diakses, ini beranimasi dengan baik, ia bekerja dengan sentuhan dan mouse, dan ini memberikan fleksibilitas desain yang hebat seperti carousel yang tidak bisa bergulir.

Tetapi ini bukan seri tutorial "Membuat Carousel Yang Cukup Bagus". Sudah waktunya kita pamer sedikit, dan untuk melakukan itu, kita punya senjata rahasia. Springs.

Kita akan menambahkan dua interaksi dengan springs. Satu untuk disentuh, dan satu untuk paginasi. Mereka berdua akan membiarkan pengguna tahu, dengan cara yang menyenangkan dan ceria, bahwa mereka telah sampai di akhir carousel.

Spring Sentuhan

Pertama, mari tambahkan tarikan style iOS saat pengguna mencoba menggulir slider melewati batas-batasnya. Saat ini, kita menutupi gulir sentuhan menggunakan clampXOffset. Sebagai gantinya, mari kita ganti ini dengan beberapa kode yang menerapkan tarikan saat offset dihitung berada di luar batas-batasnya.

Pertama, kita perlu mengimpor spring kita. Ada transformer yang disebut nonlinearSpring yang menerapkan kekuatan yang meningkat secara eksponensial terhadap jumlah yang kita berikan, menuju origin. Yang berarti semakin jauh kita menarik slider, semakin akan ditarik kembali. Kita bisa mengimpornya seperti ini:

1
const { applyOffset, clamp, nonlinearSpring, pipe } = transform;

Dalam fungsi defineDragDirection, kita memiliki kode ini:

1
action.output(pipe(
2
  ({ x }) => x,
3
  applyOffset(action.x.get(), sliderX.get()),
4
  clampXOffset,
5
  (v) => sliderX.set(v)
6
));

Tepat di atas itu, mari kita buat dua spring kita, satu untuk setiap batas gulir carousel:

1
const elasticity = 5;
2
const tugLeft = nonlinearSpring(elasticity, maxXOffset);
3
const tugRight = nonlinearSpring(elasticity, minXOffset);

Memutuskan nilai elasticity adalah masalah bermain-main saja dan melihat apa yang terasa benar. Terlalu rendah angka, dan spring akan terasa terlalu kaku. Terlalu tinggi dan kamu tidak akan melihat tarikannya, atau lebih buruk lagi, ini akan mendorong slider lebih jauh dari jari pengguna!

Sekarang kita hanya perlu menulis sebuah fungsi sederhana yang akan menerapkan salah satu spring ini jika nilai yang diberikan berada di luar kisaran yang diizinkan:

1
const applySpring = (v) => {
2
  if (v > maxXOffset) return tugLeft(v);
3
  if (v < minXOffset) return tugRight(v);
4
  return v;
5
};

Kita bisa mengganti clampXOffset pada kode diatas dengan applySpring. Sekarang, jika kamu menarik slider melewati batas-batasnya, itu akan tertarik kembali!

Namun, ketika kita melepaskan spring, itu semacam bentakan yang begitu saja kembali ke tempatnya. Kita ingin mengubah fungsi stopTouchScroll kita, yang saat ini menangani gulir momentum, untuk memeriksa apakah slider masih berada di luar rentang yang diizinkan dan, jika demikian, terapkan spring dengan aksi physics sebagai gantinya.

Spring Physics

Aksi physics juga mampu memodelkan spring. Kita hanya perlu menyediakannya dengan properti spring dan to.

Di stopTouchScroll, pindahkan inisialisasi gulir physics yang ada ke sepotong logika yang memastikan kita berada dalam batas gulir:

1
const currentX = sliderX.get();
2
3
if (currentX < minXOffset || currentX > maxXOffset) {
4
  
5
} else {
6
  action = physics({
7
    from: currentX,
8
    velocity: sliderX.getVelocity(),
9
    friction: 0.2
10
  }).output(pipe(
11
    clampXOffset,
12
    (v) => sliderX.set(v)
13
  )).start();
14
}

Dalam ketentuan pertama dari pernyataan if, kita tahu bahwa slider berada di luar batas gulir, jadi kita bisa menambahkan spring kita:

1
action = physics({
2
  from: currentX,
3
  to: (currentX < minXOffset) ? minXOffset : maxXOffset,
4
  spring: 800,
5
  friction: 0.92
6
}).output((v) => sliderX.set(v))
7
  .start();

Kita ingin membuat spring yang terasa tajam dan responsif. Saya telah memilih nilai spring yang relatif tinggi untuk memiliki "pop" yang rapat, dan saya telah menurunkan firctionnya menjadi 0.92 sehingga memungkinkan sedikit pantulan. Kamu bisa mengeset ini ke 1 untuk menghilangkan pantulan sepenuhnya.

Sebagai sedikit pekerjaan rumah, cobalah mengganti clampXOffset dalam fungsi output dari physics gulir dengan fungsi yang memicu spring yang sama saat x offset mencapai batas-batasnya. Daripada saat ini tiba-tiba berhenti, cobalah membuatnya terpental dengan lembut di akhir.

Paginasi Spring

Sentuhan pengguna selalu mendapatkan perhatian spring, bukan? Mari berbagi cinta itu dengan pengguna desktop dengan mendeteksi kapan carousel berada pada batas gulirnya, dan memiliki tangkapan indikatif untuk menunjukkan dengan jelas dan percaya diri kepada pengguna bahwa mereka berada di akhir.

Pertama, kami ingin menonaktifkan tombol paginasi saat batasnya tercapai. Pertama mari kita tambahkan aturan CSS yang menonjolkan tombol untuk menunjukkan bahwa mereka disabled. Pada aturan button, tambahkan:

1
transition: background 200ms linear;
2
3
&.disabled {
4
  background: #eee;
5
}

Kita menggunakan class di sini daripada atribut disabled yang lebih semantik karena kita masih ingin menangkap event klik, yang dimana, seperti namanya, disabled akan diblok.

Tambahkan kelas disabled ini ke tombol Prev, karena setiap carousel mulai hidup dengan offset 0:

1
<button class="prev disabled">Prev</button>

Menuju bagian atas carousel, buat fungsi baru yang disebut checkNavButtonStatus. Kita ingin fungsi ini hanya memeriksa nilai yang diberikan terhadap minXOffset dan maxXOffset dan set tombol class disabled sebagaimana yang demikian:

1
function checkNavButtonStatus(x) {
2
  if (x <= minXOffset) {
3
    nextButton.classList.add('disabled');
4
  } else {
5
    nextButton.classList.remove('disabled');
6
7
    if (x >= maxXOffset) {
8
      prevButton.classList.add('disabled');
9
    } else {
10
      prevButton.classList.remove('disabled');
11
    }
12
  }
13
}

Ini akan menggoda untuk memanggi ini setiap kali sliderX berubah. Jika kita melakukannya, tombolnya akan mulai berkedip setiap kali ada spring di sekitar batas gulir. Ini juga akan menyebabkan tindakan yang aneh jika salah satu tombol ditekan saat salah satu animasi spring. Tarikan "gulir akhir" harus selalu menyala jika kita berada di ujung carousel, sekalipun ada animasi spring yang menariknya dari ujung yang mutlak.

Jadi kita perlu lebih selektif tentang kapan harus memanggil fungsi ini. Tampaknya masuk akal untuk menyebutnya:

Pada baris terakhir onWheel, tambahkan checkNavButtonStatus(newX);.

Pada baris terakhir goto, tambahkan checkNavButtonStatus(targetX);.

Dan akhirnya, pada akhir determineDragDirection, dan dalam ketentuan gulir momentum (kode di dalam else) dari stopTouchScroll, ganti:

1
(v) => sliderX.set(v)

Dengan:

1
(v) => {
2
  sliderX.set(v);
3
  checkNavButtonStatus(v);
4
}

Sekarang yang tersisa adalah mengubah gotoPrev dan gotoNext untuk memeriksa tombol pemicu classList untuk di disabled dan hanya membuat paginasi jika itu tidak ada:

1
const gotoNext = (e) => !e.target.classList.contains('disabled')
2
  ? goto(1)
3
  : notifyEnd(-1, maxXOffset);
4
5
const gotoPrev = (e) => !e.target.classList.contains('disabled')
6
  ? goto(-1)
7
  : notifyEnd(1, minXOffset);

Fungsi notifyEnd hanyalah spring physics lainnya, dan sepertinya terlihat seperti ini:

1
function notifyEnd(delta, targetOffset) {
2
  if (action) action.stop();
3
  action = physics({
4
    from: sliderX.get(),
5
    to: targetOffset,
6
    velocity: 2000 * delta,
7
    spring: 300,
8
    friction: 0.9
9
  })
10
    .output((v) => sliderX.set(v))
11
    .start();
12
}

Miliki permainan dengan itu, dan sekali lagi, sesuaikan parameter physics yang kamu sukai.

Hanya ada satu bug kecil yang tersisa. Ketika slider springs melampaui batas paling kiri, progress bar menjadi terbalik. Kita dengan cepat dapat memperbaikinya dengan mengganti:

1
progressBarRenderer.set('scaleX', progress);

Dengan:

1
progressBarRenderer.set('scaleX', Math.max(progress, 0));

Kita bisa mencegah ini terpental ke arah lain, tetapi secara pribadi saya pikir itu cukup keren yang itu mencerminkan gerakan spring. Ini hanya tampak aneh ketika membalik ke dalam.

Bersihkan Dirimu Sendiri

Dengan aplikasi satu-halaman, website akan tahan lebih lama dalam sesi pengguna. Sering, bahkan ketika perubahan "halaman", kita masih menjalankan runtime JS sama seperti pada beban awal. Kita tidak dapat mengandalkan daftar yang bersih setiap kali pengguna mengklik link, dan itu berarti kita harus membersihkannya setelah diri kita untuk mencegah event listener menembak mati elemen.

Di React, kode ini ditempatkan di metode componentWillLeave. Vue menggunakan beforeDestroy. Ini adalah implementasi JS yang murni, tetapi kita masih dapat menyediakan metode destroy yang akan bekerja sama dalam framework.

Sejauh ini, fungsi carousel kita belum mengembalikan apa-apa. Mari kita ubah itu.

Pertama, ubah akhir baris, pada baris itu panggil carousel, ke:

1
const destroyCarousel = carousel(document.querySelector('.container'));

Kita hanya akan mengembalikan satu hal dari carousel, fungsi yang tidak mengikat semua event listener kita. Pada akhir fungsi carousel, tuliskan:

1
return () => {
2
  container.removeEventListener('touchstart', startTouchScroll);
3
  container.removeEventListener('wheel', onWheel);
4
  nextButton.removeEventListener('click', gotoNext);
5
  prevButton.removeEventListener('click', gotoPrev);
6
  slider.removeEventListener('focus', onFocus);
7
  window.removeEventListener('resize', measureCarousel);
8
};

Sekarang, jika kamu memanggil destroyCarousel dan mencoba untuk bermain dengan carousel, tidak ada yang terjadi! Hal ini hampir menjadi sedikit sedih untuk melihatnya seperti ini.

Dan Itu Saja

Wah. Itu banyak! Seberapa jauh kita telah datang. kamu dapat melihat produk akhirnya pada CodePen ini. Pada bagian akhir ini, kita telah menambahkan aksesibilitas keyboard, mengukur ulang carousel ketika perubahan viewport, beberapa penambahan yang menyenangkan dengan spring physics, dan langkah yang memilukan tetapi perlu langkah untuk meruntuhkan semuanya lagi.

Saya harap kamu menikmati tutorial ini sebanyak yang saya senang menulisnya. Saya akan senang mendengar pemikiranmu dengan cara lebih lanjut kita bisa memperbaiki aksesibilitas, atau menambahkan sedikit sentuhan yang lebih menyenangkan.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Web Design tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.