מסכי מגע זמינים ביותר ויותר מכשירים, מטלפונים ועד למסכי מחשב. האפליקציה צריכה להגיב למגע שלהם בדרכים אינטואיטיביות ויפות.
מסכי מגע זמינים ביותר ויותר מכשירים, מטלפונים ועד למסכים של מחשבים שולחניים. כשהמשתמשים בוחרים לקיים אינטראקציה עם ממשק המשתמש, האפליקציה צריכה להגיב למגע שלהם בדרכים אינטואיטיביות.
תגובה למצבי הרכיב
האם אי פעם נגעת או לחצת על אלמנט בדף אינטרנט ושאלת אם האתר מזהה אותו בפועל?
שינוי הצבע של אלמנט בזמן שהמשתמשים נוגעים בחלקים בממשק המשתמש או יוצרים איתם אינטראקציה, נותן ביטחון בסיסי שהאתר שלך פועל. מעבר לזה, מפחית תסכול, הוא גם יכול להעניק תחושה מהירה ורספונסיבית.
רכיבי DOM יכולים לקבל בירושה כל אחד מהמצבים הבאים: ברירת מחדל, מיקוד, העברת עכבר ופעיל. כדי לשנות את ממשק המשתמש שלנו לכל אחד מהמצבים האלה, צריך להחיל סגנונות על פסאודו המחלקות הבאות: :hover
, :focus
ו-:active
, כפי שמוצג כאן:
.btn {
background-color: #4285f4;
}
.btn:hover {
background-color: #296cdb;
}
.btn:focus {
background-color: #0f52c1;
/* The outline parameter suppresses the border
color / outline when focused */
outline: 0;
}
.btn:active {
background-color: #0039a8;
}
ברוב הדפדפנים בנייד, מצבי hover ו/או hover יחולו על רכיב מסוים אחרי שמקישים עליו.
מומלץ לחשוב היטב איזה סגנונות להגדיר ואיך הם ייראו למשתמש אחרי שהוא יסתיים.
התעלמות מסגנונות ברירת המחדל של הדפדפן
אחרי שמוסיפים סגנונות למצבים השונים, תבחינו שרוב הדפדפנים מטמיעים סגנונות משלהם בהתאם למגע של המשתמש. הסיבה העיקרית לכך היא
כשמכשירים ניידים הושקו לראשונה, לחלק מהאתרים לא היה עיצוב למצב :active
. כתוצאה מכך, דפדפנים רבים הוסיפו עוד צבע או סגנון להדגשה על מנת לתת למשתמש משוב.
רוב הדפדפנים משתמשים במאפיין ה-CSS outline
כדי להציג טבעת מסביב לרכיב כשהרכיב מתמקד בו. אפשר להסתיר אותו באמצעות:
.btn:focus {
outline: 0;
/* Add replacement focus styling here (i.e. border) */
}
ב-Safari וב-Chrome מוסיפים צבע להדגשה בהקשה, שניתן למנוע באמצעות
מאפיין ה-CSS -webkit-tap-highlight-color
:
/* Webkit / Chrome Specific CSS to remove tap
highlight color */
.btn {
-webkit-tap-highlight-color: transparent;
}
ל-Internet Explorer ב-Windows Phone יש התנהגות דומה, אבל הוא מבוטל באמצעות מטא תג:
<meta name="msapplication-tap-highlight" content="no">
ל-Firefox יש שתי תופעות לוואי לטיפול.
פסאודו המחלקה -moz-focus-inner
, שמוסיפה קו מתאר לאלמנטים שניתן לגעת בהם, אפשר להסיר אותם על ידי הגדרת border: 0
.
אם משתמשים באלמנט <button>
ב-Firefox, מיושמת הדרגתית, ואפשר להסיר אותה על ידי הגדרת background-image: none
.
/* Firefox Specific CSS to remove button
differences and focus ring */
.btn {
background-image: none;
}
.btn::-moz-focus-inner {
border: 0;
}
השבתת 'בחירת משתמשים'
כשיוצרים ממשק משתמש, יכולים להיות תרחישים שבהם תרצו שהמשתמשים ייצרו אינטראקציה עם הרכיבים, אבל תרצו לבטל את התנהגות ברירת המחדל של בחירת טקסט בלחיצה ארוכה או גרירת העכבר מעל לממשק המשתמש.
אפשר לעשות זאת באמצעות מאפיין ה-CSS user-select
, אבל חשוב לשים לב שביצוע פעולה זו בתוכן עלול להכעיס extremely את המשתמשים אם הם רוצים לבחור את הטקסט ברכיב.
לכן צריך להשתמש בו בזהירות ובצמצום.
/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
user-select: none;
}
הטמעת תנועות מותאמות אישית
אם יש לכם רעיון לאינטראקציות ולתנועות מותאמות אישית באתר, יש שני נושאים שכדאי לזכור:
- הנחיות לתמיכה בכל הדפדפנים.
- איך לשמור על קצב פריימים גבוה.
במאמר הזה נציג בדיוק את הנושאים האלה, שעוסקים ב-API שבו אנחנו צריכים לתמוך על מנת להגיע לכל הדפדפנים, ואז נסביר איך אנחנו משתמשים באירועים האלה ביעילות.
בהתאם למה שאתם רוצים שהתנועה תבצע, סביר להניח שתרצו שהמשתמש יבצע אינטראקציה עם רכיב אחד בכל פעם או שתרצו שהוא יוכל לקיים אינטראקציה עם מספר רכיבים בו-זמנית.
נבחן שתי דוגמאות במאמר זה – כדי להדגים תמיכה בכל הדפדפנים ואיך לשמור על קצב פריימים גבוה.
הדוגמה הראשונה תאפשר למשתמש לבצע אינטראקציה עם רכיב אחד. במקרה כזה, יכול להיות שתרצו שכל אירועי המגע יוענקו לאלמנט הזה, כל עוד התנועה התחילה בהתחלה על האלמנט עצמו. לדוגמה, גם אם תזיזו את האצבע מהרכיב שאפשר להחליק, עדיין תוכלו לשלוט בו.
האפשרות הזו שימושית כי היא מאפשרת למשתמשים הרבה גמישות, אבל מגבילה את היכולת שלהם לקיים אינטראקציה עם ממשק המשתמש.
לעומת זאת, אם אתם מצפים שמשתמשים יוכלו לקיים אינטראקציה עם מספר רכיבים בו-זמנית (באמצעות ריבוי נקודות מגע), עליכם להגביל את המגע לרכיב הספציפי.
זו שיטה גמישה יותר למשתמשים, אבל היא מסבכת את הלוגיקה למניפולציה של ממשק המשתמש ופחות עמידה בפני שגיאות משתמשים.
הוספת פונקציות event listener
ב-Chrome (מגרסה 55 ואילך), Internet Explorer ו-Edge, PointerEvents
היא הגישה המומלצת להטמעת תנועות מותאמות אישית.
בדפדפנים אחרים, TouchEvents
ו-MouseEvents
הם הגישה הנכונה.
היתרון הנהדר של PointerEvents
הוא שהוא ממזג כמה סוגי קלט, כולל אירועי עכבר, מגע ועט, לקבוצה אחת של קריאה חוזרת. האירועים שצריך להאזין להם הם pointerdown
, pointermove
, pointerup
ו-pointercancel
.
האפליקציות המקבילות בדפדפנים אחרים הן touchstart
, touchmove
, touchend
ו-touchcancel
לאירועי מגע. אם רוצים להטמיע את אותה תנועה לקלט עכבר, צריך להטמיע את mousedown
, את mousemove
ואת mouseup
.
אם יש לכם שאלות לגבי האירועים שבהם כדאי להשתמש, תוכלו להיעזר בטבלה הבאה אירועי מגע, עכבר ומצביע.
כדי להשתמש באירועים האלה צריך לקרוא לשיטה addEventListener()
ברכיב DOM, לצד שם האירוע, פונקציית קריאה חוזרת ופרמטר בוליאני.
הערך הבוליאני קובע אם צריך לתפוס את האירוע לפני או אחרי שאלמנטים אחרים קיבלו הזדמנות לקלוט ולפרש את האירועים. (המשמעות של true
היא שהאירוע לפני רכיבים אחרים).
הנה דוגמה להאזנה להתחלת אינטראקציה.
// Check if pointer events are supported.
if (window.PointerEvent) {
// Add Pointer Event Listener
swipeFrontElement.addEventListener('pointerdown', this.handleGestureStart, true);
swipeFrontElement.addEventListener('pointermove', this.handleGestureMove, true);
swipeFrontElement.addEventListener('pointerup', this.handleGestureEnd, true);
swipeFrontElement.addEventListener('pointercancel', this.handleGestureEnd, true);
} else {
// Add Touch Listener
swipeFrontElement.addEventListener('touchstart', this.handleGestureStart, true);
swipeFrontElement.addEventListener('touchmove', this.handleGestureMove, true);
swipeFrontElement.addEventListener('touchend', this.handleGestureEnd, true);
swipeFrontElement.addEventListener('touchcancel', this.handleGestureEnd, true);
// Add Mouse Listener
swipeFrontElement.addEventListener('mousedown', this.handleGestureStart, true);
}
טיפול באינטראקציה של רכיב יחיד
בקטע הקוד הקצר שלמעלה, הוספנו רק את ה-event listener הפותח לאירועי עכבר. הסיבה לכך היא שאירועי העכבר יוקפצו רק כשמעבירים את העכבר מעל לאלמנט שאליו מתווסף event listener.
TouchEvents
יעקוב אחרי תנועה אחרי שהיא מתחילה, בלי קשר למקום שבו התרחש הנגיעה, ו-PointerEvents
יעקוב אחרי אירועים בלי קשר למקום שבו התרחש הנגיעה אחרי קריאה ל-setPointerCapture
ברכיב DOM.
עבור אירועי העברת עכבר וסיום, אנחנו מוסיפים את פונקציות event listener בשיטת ההתחלה של התנועות ומוסיפים את ה-listener למסמך, כלומר הוא יכול לעקוב אחרי הסמן עד להשלמת התנועה.
כדי ליישם זאת:
- הוספת כל המאזינים של TouchEvent ו-PointerEvent. ב-MouseEvents, מוסיפים רק את אירוע ההתחלה.
- בתוך הקריאה החוזרת (callback) של תנועת ההתחלה, מקשרים את תנועת העכבר ומסיימים אירועים
למסמך. כך כל אירועי העכבר מתקבלים, גם אם האירוע התרחש ברכיב המקורי וגם אם לא. עבור PointerEvents, אנחנו צריכים להפעיל את
setPointerCapture()
ברכיב המקורי שלנו כדי לקבל את כל האירועים הנוספים. לאחר מכן יש לטפל בהתחלת התנועה. - טיפול באירועי ההעברה.
- באירוע הסיום, יש להסיר את העברת העכבר, לסיים את ה-listener מהמסמך ולסיים את התנועה.
בהמשך מופיע קטע של השיטה handleGestureStart()
שמוסיפה למסמך את אירועי ההעברה והסיום:
// Handle the start of gestures
this.handleGestureStart = function(evt) {
evt.preventDefault();
if(evt.touches && evt.touches.length > 1) {
return;
}
// Add the move and end listeners
if (window.PointerEvent) {
evt.target.setPointerCapture(evt.pointerId);
} else {
// Add Mouse Listeners
document.addEventListener('mousemove', this.handleGestureMove, true);
document.addEventListener('mouseup', this.handleGestureEnd, true);
}
initialTouchPos = getGesturePointFromEvent(evt);
swipeFrontElement.style.transition = 'initial';
}.bind(this);
הקריאה החוזרת (callback) שנוספת היא handleGestureEnd()
, שמסירה את ה-event listener להעברה ולסיום מהמסמך ומשחררת את תיעוד הסמן
כשהתנועה מסתיימת כך:
// Handle end gestures
this.handleGestureEnd = function(evt) {
evt.preventDefault();
if (evt.touches && evt.touches.length > 0) {
return;
}
rafPending = false;
// Remove Event Listeners
if (window.PointerEvent) {
evt.target.releasePointerCapture(evt.pointerId);
} else {
// Remove Mouse Listeners
document.removeEventListener('mousemove', this.handleGestureMove, true);
document.removeEventListener('mouseup', this.handleGestureEnd, true);
}
updateSwipeRestPosition();
initialTouchPos = null;
}.bind(this);
בהתאם לדפוס הזה של הוספת אירוע ההעברה למסמך, אם משתמש מתחיל אינטראקציה עם רכיב ומזיז את התנועה שלו אל מחוץ לאלמנט, נמשיך לקבל תנועות עכבר ללא קשר למיקום של הדף, מכיוון שהאירועים מתקבלים מהמסמך.
בתרשים הזה רואים מה עושים אירועי המגע בזמן שאנחנו מוסיפים את אירועי ההעברה והסיום למסמך ברגע שמתחילה תנועה.
תגובה יעילה למגע
עכשיו, אחרי שטיפלנו באירועי ההתחלה והסיום, אנחנו יכולים להגיב לאירועי המגע.
לכל אירוע התחלה והעברה, אפשר לחלץ בקלות את x
ואת y
מאירוע.
הדוגמה הבאה מוודאת שהאירוע הוא מ-TouchEvent
על ידי בדיקה אם האירוע targetTouches
קיים. אם כן, הוא מחלץ את
clientX
ו-clientY
מהנגיעה הראשונה.
אם האירוע הוא PointerEvent
או MouseEvent
, הוא מחלץ את clientX
ואת clientY
ישירות מהאירוע עצמו.
function getGesturePointFromEvent(evt) {
var point = {};
if (evt.targetTouches) {
// Prefer Touch Events
point.x = evt.targetTouches[0].clientX;
point.y = evt.targetTouches[0].clientY;
} else {
// Either Mouse event or Pointer Event
point.x = evt.clientX;
point.y = evt.clientY;
}
return point;
}
ל-TouchEvent
יש שלוש רשימות שמכילות נתוני מגע:
touches
: רשימה של כל הנגיעות הנוכחיות במסך, בלי קשר לרכיב ה-DOM שבו.targetTouches
: רשימת נגיעות ברכיב ה-DOM שאליו האירוע קשור.changedTouches
: רשימת נגיעות שהשתנו והובילו להפעלת האירוע.
ברוב המקרים, ב-targetTouches
יש את כל מה שצריך. (למידע נוסף על הרשימות האלה, ראו רשימות מגע).
שימוש ב-requestAnimationFrame
הקריאות החוזרות של האירועים מופעלות ב-thread הראשי, ולכן אנחנו רוצים להריץ בקריאות החוזרות (callback) כמה שיותר קוד – כדי לשמור על קצב פריימים גבוה ולמנוע בעיות בממשק (jank).
באמצעות requestAnimationFrame()
יש לנו הזדמנות לעדכן את ממשק המשתמש ממש לפני שהדפדפן מתכוון לשרטט מסגרת, והוא יעזור לנו להוציא חלק מהקריאות החוזרות של האירועים.
אם אתם לא מכירים את requestAnimationFrame()
, מידע נוסף זמין כאן.
הטמעה אופיינית היא שמירת הקואורדינטות x
ו-y
מאירועי ההתחלה וההעברה, ולבקש מסגרת אנימציה בתוך הקריאה החוזרת לאירוע ההעברה.
בהדגמה שלנו, אנחנו מאחסנים את מיקום המגע הראשוני ב-handleGestureStart()
(חיפוש initialTouchPos
):
// Handle the start of gestures
this.handleGestureStart = function(evt) {
evt.preventDefault();
if (evt.touches && evt.touches.length > 1) {
return;
}
// Add the move and end listeners
if (window.PointerEvent) {
evt.target.setPointerCapture(evt.pointerId);
} else {
// Add Mouse Listeners
document.addEventListener('mousemove', this.handleGestureMove, true);
document.addEventListener('mouseup', this.handleGestureEnd, true);
}
initialTouchPos = getGesturePointFromEvent(evt);
swipeFrontElement.style.transition = 'initial';
}.bind(this);
השיטה handleGestureMove()
שומרת את מיקום האירוע לפני שמבקשים פריים אנימציה, במקרה הצורך, ומעבירה את הפונקציה onAnimFrame()
כקריאה חוזרת:
this.handleGestureMove = function (evt) {
evt.preventDefault();
if (!initialTouchPos) {
return;
}
lastTouchPos = getGesturePointFromEvent(evt);
if (rafPending) {
return;
}
rafPending = true;
window.requestAnimFrame(onAnimFrame);
}.bind(this);
הערך onAnimFrame
הוא פונקציה שכאשר מפעילים אותה, היא משנה את ממשק המשתמש שלנו כדי להזיז אותו. על ידי העברת הפונקציה הזו אל requestAnimationFrame()
, אנחנו מנחים את הדפדפן לקרוא לפונקציה ממש לפני שהוא עומד לעדכן את הדף (כלומר, לצבוע שינויים בדף).
בקריאה החוזרת (callback) של handleGestureMove()
אנחנו בודקים בהתחלה אם rafPending
מוגדר כ-False, שמציין אם בוצעה קריאה ל-onAnimFrame()
על ידי requestAnimationFrame()
מאז אירוע ההעברה האחרון. כלומר, בכל רגע נתון יש לנו רק requestAnimationFrame()
אחד שממתין להפעלה.
כשמתבצעת קריאה חוזרת (callback) של onAnimFrame()
, אנחנו מגדירים את הטרנספורמציה בכל הרכיבים שאנחנו רוצים להעביר לפני שמעדכנים את rafPending
ל-false
, כדי לאפשר לאירוע המגע הבא לבקש מסגרת אנימציה חדשה.
function onAnimFrame() {
if (!rafPending) {
return;
}
var differenceInX = initialTouchPos.x - lastTouchPos.x;
var newXTransform = (currentXPosition - differenceInX)+'px';
var transformStyle = 'translateX('+newXTransform+')';
swipeFrontElement.style.webkitTransform = transformStyle;
swipeFrontElement.style.MozTransform = transformStyle;
swipeFrontElement.style.msTransform = transformStyle;
swipeFrontElement.style.transform = transformStyle;
rafPending = false;
}
שליטה בתנועות באמצעות פעולות מגע
מאפיין ה-CSS touch-action
מאפשר לשלוט בהתנהגות המגע המוגדרת כברירת מחדל ברכיב. בדוגמאות שלנו אנחנו משתמשים ב-touch-action: none
כדי למנוע מהדפדפן לעשות כל פעולה במגע של משתמשים, וכך ליירט את כל אירועי המגע.
/* Pass all touches to javascript: */
button.custom-touch-logic {
touch-action: none;
}
השימוש ב-touch-action: none
הוא אפשרות גרעינית במידה מסוימת, כי הוא מונע את כל התנהגויות ברירת המחדל של הדפדפן. במקרים רבים, אחת מהאפשרויות שבהמשך היא פתרון טוב יותר.
באמצעות touch-action
אפשר להשבית תנועות שהוטמעו בדפדפן.
לדוגמה, IE10+ תומך בתנועת הקשה כפולה לשינוי מרחק התצוגה. הגדרה של touch-action
של manipulation
מונעת את התנהגות ברירת המחדל של הקשה כפולה.
כך תהיה לך אפשרות להטמיע תנועה של הקשה כפולה בעצמך.
בהמשך מופיעה רשימה של ערכי touch-action
נפוצים:
תמיכה בגרסאות ישנות יותר של IE
אם רוצים לתמוך ב-IE10, צריך לטפל בגרסאות עם קידומת של הספק של PointerEvents
.
בדרך כלל, מומלץ לחפש window.PointerEvent
על מנת לבדוק אם יש תמיכה ב-PointerEvents
, אבל ב-IE10 עדיף לחפש window.navigator.msPointerEnabled
.
שמות האירועים עם קידומות של ספקים הם: 'MSPointerDown'
, 'MSPointerUp'
ו-'MSPointerMove'
.
הדוגמה הבאה ממחישה איך לבדוק אם יש תמיכה ולשנות את שמות האירועים.
var pointerDownName = 'pointerdown';
var pointerUpName = 'pointerup';
var pointerMoveName = 'pointermove';
if (window.navigator.msPointerEnabled) {
pointerDownName = 'MSPointerDown';
pointerUpName = 'MSPointerUp';
pointerMoveName = 'MSPointerMove';
}
// Simple way to check if some form of pointerevents is enabled or not
window.PointerEventsSupport = false;
if (window.PointerEvent || window.navigator.msPointerEnabled) {
window.PointerEventsSupport = true;
}
למידע נוסף, קראו את המאמר הזה על עדכון מ-Microsoft.
חומרי עזר
מחלקות פסאודוגרפיות למצבי מגע
ההפניה הטובה ביותר לאירועי מגע נמצאת כאן: אירועי מגע של W3C.
אירועי מגע, עכבר וסמן
האירועים האלה הם אבני הבניין להוספת תנועות חדשות לאפליקציה:
רשימות מגע
כל אירוע מגע כולל שלושה מאפייני רשימה:
הפעלת תמיכה במצבים פעילים ב-iOS
לצערנו, Safari ב-iOS לא מחיל את המצב active כברירת מחדל. כדי שהוא יפעל, צריך להוסיף פונקציות event listener ל-touchstart
לגוף המסמך או לכל רכיב.
צריך לבצע את הבדיקה באמצעות בדיקה של סוכן המשתמש, כך שהיא תפעל רק במכשירי iOS.
היתרון של הוספת התחלת מגע לגוף הוא החלה על כל הרכיבים ב-DOM, אבל עשויות להיות בעיות בביצועים בזמן הגלילה בדף.
window.onload = function() {
if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
document.body.addEventListener('touchstart', function() {}, false);
}
};
החלופה היא להוסיף את המאזינים להתחלת המגע לכל הרכיבים האינטראקטיביים בדף, וכך להפחית חלק מהבעיות בביצועים.
window.onload = function() {
if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
var elements = document.querySelectorAll('button');
var emptyFunction = function() {};
for (var i = 0; i < elements.length; i++) {
elements[i].addEventListener('touchstart', emptyFunction, false);
}
}
};