מסכי מגע זמינים במכשירים רבים יותר, החל מטלפונים ועד למסכים במחשבים. האפליקציה צריכה להגיב למגע שלהם בצורה אינטואיטיבית ויפה.
מסכי מגע זמינים ביותר ויותר מכשירים, מטלפונים ועד מסכים של מחשבים. כשהמשתמשים בוחרים לבצע פעולות בממשק המשתמש, האפליקציה צריכה להגיב באופן אינטואיטיבי למשתמשים.
תגובה למצבים של רכיבים
האם אי פעם נגעתם או לחצתם על רכיב בדף אינטרנט ושאלתם את עצמכם אם האתר אכן זיהה אותו?
שינוי פשוט של צבע רכיב כשמשתמשים נוגעים בו או מבצעים אינטראקציה עם חלקים בממשק המשתמש שלכם נותן להם ביטחון בסיסי שהאתר פועל. כך תוכלו להפחית את תסכולם של המשתמשים, וגם ליצור תחושה של מהירות תגובה.
רכיבי 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;
}
ברוב הדפדפנים בנייד, מצבי העברת העכבר ו/או מיקוד יחולו על רכיב אחרי שמקישים עליו.
חשוב לבחור בקפידה את הסגנונות שתגדירו ואת המראה שלהם בעיני המשתמש אחרי שהוא יסיים את המגע.
ביטול סגנונות ברירת המחדל של הדפדפן
אחרי שתוסיפו סגנונות למצבים השונים, תבחינו שרוב הדפדפנים מטמיעים סגנונות משלהם בתגובה למגע של המשתמש. הסיבה העיקרית לכך היא שבזמן ההשקה הראשונית של מכשירים ניידים, לאתרים מסוימים לא היה עיצוב למצב :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
, אבל חשוב לזכור שמאוד מעצבן משתמשים אם הם רוצים לבחור את הטקסט ברכיב.
לכן חשוב להשתמש בה בזהירות ובמידה.
/* 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
הוא שהוא משללב כמה סוגים של קלט, כולל אירועי עכבר, מגע ועט, לקבוצה אחת של קריאות חזרה (callbacks). האירועים שצריך להאזין להם הם pointerdown
, pointermove
, pointerup
ו-pointercancel
.
הערכים המקבילים בדפדפנים אחרים הם touchstart
, touchmove
, touchend
ו-touchcancel
לאירועי מגע. אם רוצים להטמיע את אותה תנועה להזנת עכבר, צריך להטמיע את הערכים mousedown
, mousemove
ו-mouseup
.
אם יש לכם שאלות לגבי האירועים שבהם כדאי להשתמש, תוכלו לעיין בטבלה הזו של אירועי מגע, עכבר ומצביע.
כדי להשתמש באירועים האלה, צריך להפעיל את השיטה addEventListener()
על רכיב DOM, יחד עם שם האירוע, פונקציית קריאה חוזרת (callback) ומשתנה בוליאני.
הערך הבוליאני קובע אם צריך לתפוס את האירוע לפני או אחרי שרכיבים אחרים יכלו לתפוס ולפרש את האירועים. (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 של TouchEvent ו-PointerEvent. ב-MouseEvents, מוסיפים רק את אירוע ההתחלה.
- בתוך פונקציית ה-callback של תנועת ההתחלה, מקשרים את אירועי העברת העכבר ואת אירועי הסיום למסמך. באופן הזה, כל אירועי העכבר מתקבלים, גם אם האירוע מתרחש ברכיב המקורי וגם אם לא. כדי לקבל את כל האירועים הבאים ב-PointerEvents, צריך להפעיל את
setPointerCapture()
על הרכיב המקורי. לאחר מכן מטפלים בהתחלת התנועה. - טיפול באירועי ההעברה.
- באירוע הסיום, מסירים מהמסמך את האירועים של תנועת העכבר והאירוע של הסיום, ומסיימים את התנועות.
בהמשך מופיע קטע קוד של השיטה 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);
הקריאה החוזרת לסיום שאנחנו מוסיפים היא handleGestureEnd()
, והיא מסירה מהמסמך את פונקציות ההעברה והסיום של מאזינים לאירוע, ולאחר מכן משחררת את תיעוד הסמן אחרי שהתנועה מסתיימת:
// 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
מאחר שהקריאות החוזרות של האירועים מופעלות בשרשור הראשי, אנחנו רוצים להריץ כמה שפחות קוד בקריאות החוזרות של האירועים שלנו, כדי לשמור על קצב פריימים גבוה ולמנוע תנודות.
באמצעות requestAnimationFrame()
יש לנו הזדמנות לעדכן את ממשק המשתמש ממש לפני שהדפדפן מתכוון לצייר מסגרת, וכך נוכל להעביר חלק מהעבודה מהקריאות החוזרות (callbacks) של האירועים.
אם אתם לא מכירים את requestAnimationFrame()
, תוכלו לקרוא מידע נוסף כאן.
הטמעה אופיינית היא לשמור את הקואורדינטות x
ו-y
מאירועי ההתחלה וההעברה ולבקש פריים אנימציה בתוך הקריאה החוזרת של האירוע Transfer (העברה).
בדמו שלנו, אנחנו שומרים את מיקום המגע הראשוני ב-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, כדי לבדוק אם requestAnimationFrame()
קרא ל-onAnimFrame()
מאז אירוע ההזזה האחרון. המשמעות היא שיש לנו רק 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;
}
שליטה בתנועות באמצעות פעולות מגע
המאפיין touch-action
ב-CSS מאפשר לכם לקבוע את התנהגות ברירת המחדל של רכיב במגע. בדוגמאות שלנו אנחנו משתמשים ב-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
עם תחילית של ספק.
כדי לבדוק אם יש תמיכה ב-PointerEvents
, בדרך כלל מחפשים את הערך window.PointerEvent
, אבל ב-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 לא מיושם כברירת מחדל. כדי שהמצב יפעל, צריך להוסיף מאזין לאירועים מסוג 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);
}
}
};