Màn hình cảm ứng ngày càng xuất hiện trên nhiều thiết bị, từ điện thoại cho đến màn hình máy tính. Ứng dụng của bạn phải phản hồi thao tác chạm của họ theo cách trực quan và đẹp mắt.
Màn hình cảm ứng ngày càng xuất hiện trên nhiều thiết bị, từ điện thoại đến màn hình máy tính. Khi người dùng chọn tương tác với giao diện người dùng, ứng dụng của bạn phải phản hồi thao tác chạm của họ theo cách trực quan.
Phản hồi trạng thái phần tử
Bạn đã bao giờ chạm hoặc nhấp vào một phần tử trên trang web và đặt câu hỏi liệu trang web có thực sự phát hiện thấy phần tử đó hay không?
Chỉ cần thay đổi màu của một thành phần khi người dùng chạm hoặc tương tác với các phần trên giao diện người dùng, bạn đã có thể đảm bảo rằng trang web của mình đang hoạt động. Điều này không chỉ giúp giảm bớt sự thất vọng mà còn mang lại cảm giác nhanh chóng và linh hoạt.
Các phần tử DOM có thể kế thừa bất kỳ trạng thái nào sau đây: mặc định, tiêu điểm, di chuột và đang hoạt động. Để thay đổi giao diện người dùng cho từng trạng thái này, chúng ta cần áp dụng kiểu cho các lớp giả sau đây là :hover
, :focus
và :active
như minh hoạ bên dưới:
.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;
}
Trên hầu hết các trình duyệt di động, trạng thái di chuột và/hoặc tập trung sẽ áp dụng cho một phần tử sau khi phần tử đó được nhấn vào.
Hãy cân nhắc kỹ những kiểu bạn đặt và giao diện của các kiểu đó đối với người dùng sau khi họ hoàn tất thao tác chạm.
Chặn các kiểu trình duyệt mặc định
Sau khi thêm các kiểu cho các trạng thái khác nhau, bạn sẽ nhận thấy hầu hết các trình duyệt đều triển khai các kiểu riêng để phản hồi thao tác chạm của người dùng. Điều này phần lớn là do khi thiết bị di động ra mắt lần đầu tiên, một số trang web không định kiểu cho trạng thái :active
. Do đó, nhiều trình duyệt đã thêm màu hoặc kiểu làm nổi bật để đưa ra phản hồi cho người dùng.
Hầu hết các trình duyệt đều sử dụng thuộc tính CSS outline
để hiển thị một vòng tròn xung quanh một phần tử khi phần tử đó được lấy tiêu điểm. Bạn có thể ngăn chặn lỗi này bằng:
.btn:focus {
outline: 0;
/* Add replacement focus styling here (i.e. border) */
}
Safari và Chrome thêm màu nhấn khi nhấn. Bạn có thể ngăn việc này bằng thuộc tính CSS -webkit-tap-highlight-color
:
/* Webkit / Chrome Specific CSS to remove tap
highlight color */
.btn {
-webkit-tap-highlight-color: transparent;
}
Internet Explorer trên Windows Phone cũng có hành vi tương tự, nhưng bị chặn thông qua thẻ meta:
<meta name="msapplication-tap-highlight" content="no">
Firefox có hai tác dụng phụ cần xử lý.
Bạn có thể xoá lớp giả lập -moz-focus-inner
(thêm đường viền trên các phần tử có thể chạm) bằng cách đặt border: 0
.
Nếu đang sử dụng phần tử <button>
trên Firefox, bạn sẽ thấy một hiệu ứng chuyển màu (gradient) được áp dụng. Bạn có thể xoá phần tử này bằng cách đặt background-image: none
.
/* Firefox Specific CSS to remove button
differences and focus ring */
.btn {
background-image: none;
}
.btn::-moz-focus-inner {
border: 0;
}
Tắt tính năng chọn của người dùng
Khi tạo giao diện người dùng, có thể có những trường hợp bạn muốn người dùng tương tác với các phần tử của bạn nhưng bạn muốn ngăn hành vi mặc định là chọn văn bản khi nhấn và giữ hoặc kéo chuột qua giao diện người dùng.
Bạn có thể thực hiện việc này bằng thuộc tính CSS user-select
, nhưng hãy lưu ý rằng việc làm này trên nội dung có thể khiến người dùng vô cùng tức giận nếu họ muốn chọn văn bản trong phần tử.
Vì vậy, hãy nhớ sử dụng tính năng này một cách thận trọng và tiết kiệm.
/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
user-select: none;
}
Triển khai cử chỉ tuỳ chỉnh
Nếu bạn có ý tưởng về các cử chỉ và lượt tương tác tuỳ chỉnh cho trang web của mình, hãy lưu ý hai chủ đề sau:
- Cách hỗ trợ tất cả trình duyệt.
- Cách duy trì tốc độ khung hình cao.
Trong bài viết này, chúng ta sẽ xem xét chính xác các chủ đề này, bao gồm cả API mà chúng ta cần hỗ trợ để tiếp cận tất cả trình duyệt, sau đó sẽ đề cập đến cách sử dụng các sự kiện này một cách hiệu quả.
Tuỳ thuộc vào việc bạn muốn cử chỉ làm gì, có thể bạn muốn người dùng tương tác với một phần tử tại một thời điểm hoặc bạn muốn họ có thể tương tác với nhiều phần tử cùng một lúc.
Chúng ta sẽ xem xét hai ví dụ trong bài viết này, cả hai đều minh hoạ việc hỗ trợ tất cả trình duyệt và cách duy trì tốc độ khung hình cao.
Ví dụ đầu tiên sẽ cho phép người dùng tương tác với một phần tử. Trong trường hợp này, bạn có thể muốn tất cả sự kiện chạm được cung cấp cho một phần tử đó, miễn là cử chỉ ban đầu bắt đầu trên chính phần tử đó. Ví dụ: việc di chuyển ngón tay ra khỏi phần tử có thể vuốt vẫn có thể điều khiển phần tử đó.
Điều này rất hữu ích vì mang lại sự linh hoạt cho người dùng, nhưng đồng thời cũng áp dụng một hạn chế về cách người dùng có thể tương tác với giao diện người dùng của bạn.
Tuy nhiên, nếu bạn muốn người dùng tương tác với nhiều phần tử cùng một lúc (sử dụng tính năng cảm ứng đa điểm), bạn nên hạn chế thao tác chạm vào một phần tử cụ thể.
Cách này linh hoạt hơn cho người dùng, nhưng làm phức tạp logic thao tác với giao diện người dùng và ít linh hoạt hơn trước lỗi của người dùng.
Thêm trình nghe sự kiện
Trong Chrome (phiên bản 55 trở lên), Internet Explorer và Edge, bạn nên sử dụng PointerEvents
để triển khai các cử chỉ tuỳ chỉnh.
Trong các trình duyệt khác, TouchEvents
và MouseEvents
là phương pháp chính xác.
Tính năng tuyệt vời của PointerEvents
là hợp nhất nhiều kiểu phương thức nhập, bao gồm cả sự kiện bằng chuột, chạm và bút, thành một tập hợp lệnh gọi lại. Các sự kiện cần theo dõi là pointerdown
, pointermove
, pointerup
và pointercancel
.
Các đối tượng tương đương trong các trình duyệt khác là touchstart
, touchmove
,
touchend
và touchcancel
cho các sự kiện chạm và nếu bạn muốn triển khai
cùng một cử chỉ cho hoạt động nhập bằng chuột, bạn cần triển khai mousedown
,
mousemove
và mouseup
.
Nếu bạn có thắc mắc về những sự kiện cần sử dụng, hãy xem bảng Sự kiện chạm, chuột và con trỏ.
Để sử dụng các sự kiện này, bạn cần gọi phương thức addEventListener()
trên một phần tử DOM, cùng với tên của một sự kiện, một hàm gọi lại và một boolean.
Giá trị boolean xác định xem bạn nên phát hiện sự kiện trước hay sau khi các phần tử khác có cơ hội phát hiện và diễn giải sự kiện. (true
có nghĩa là bạn muốn sự kiện diễn ra trước các phần tử khác.)
Dưới đây là ví dụ về cách lắng nghe để bắt đầu một tương tác.
// 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);
}
Xử lý lượt tương tác với một phần tử
Trong đoạn mã ngắn ở trên, chúng ta chỉ thêm trình nghe sự kiện bắt đầu cho các sự kiện chuột. Lý do là các sự kiện chuột sẽ chỉ kích hoạt khi con trỏ di chuột qua phần tử mà trình nghe sự kiện được thêm vào.
TouchEvents
sẽ theo dõi một cử chỉ sau khi thao tác đó bắt đầu, bất kể vị trí chạm xảy ra và PointerEvents
sẽ theo dõi các sự kiện bất kể vị trí chạm xảy ra sau khi chúng ta gọi setPointerCapture
trên phần tử DOM.
Đối với các sự kiện di chuyển và kết thúc chuột, chúng ta thêm trình nghe sự kiện trong phương thức bắt đầu cử chỉ và thêm trình nghe vào tài liệu, nghĩa là trình nghe có thể theo dõi con trỏ cho đến khi cử chỉ hoàn tất.
Các bước triển khai như sau:
- Thêm tất cả trình nghe TouchEvent và PointerEvent. Đối với MouseEvents, hãy thêm chỉ sự kiện bắt đầu.
- Bên trong lệnh gọi lại bắt đầu cử chỉ, hãy liên kết các sự kiện di chuyển chuột và kết thúc với tài liệu. Bằng cách này, tất cả sự kiện chuột đều được nhận, bất kể sự kiện có xảy ra trên phần tử ban đầu hay không. Đối với PointerEvents, chúng tôi cần gọi
setPointerCapture()
trên phần tử ban đầu để nhận tất cả các sự kiện tiếp theo. Sau đó, hãy xử lý phần bắt đầu của cử chỉ. - Xử lý các sự kiện di chuyển.
- Trên sự kiện kết thúc, hãy xoá trình nghe di chuyển chuột và kết thúc khỏi tài liệu rồi kết thúc cử chỉ.
Dưới đây là đoạn mã của phương thức handleGestureStart()
. Phương thức này sẽ thêm
các sự kiện di chuyển và kết thúc vào tài liệu:
// 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);
Lệnh gọi lại kết thúc mà chúng ta thêm là handleGestureEnd()
. Lệnh này sẽ xoá trình nghe sự kiện di chuyển và kết thúc khỏi tài liệu, đồng thời phát hành tính năng chụp con trỏ khi cử chỉ kết thúc như sau:
// 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);
Bằng cách làm theo mẫu này để thêm sự kiện di chuyển vào tài liệu, nếu người dùng bắt đầu tương tác với một phần tử và di chuyển cử chỉ của họ ra khỏi phần tử đó, chúng ta sẽ tiếp tục nhận được các chuyển động của chuột bất kể vị trí của chúng trên trang, vì các sự kiện đang được nhận từ tài liệu.
Sơ đồ này cho thấy những gì các sự kiện chạm đang thực hiện khi chúng ta thêm sự kiện di chuyển và kết thúc vào tài liệu sau khi một cử chỉ bắt đầu.
Phản hồi thao tác chạm một cách hiệu quả
Giờ đây, khi đã xử lý xong sự kiện bắt đầu và kết thúc, chúng ta có thể thực sự phản hồi các sự kiện chạm.
Đối với bất kỳ sự kiện bắt đầu và di chuyển nào, bạn có thể dễ dàng trích xuất x
và y
từ một sự kiện.
Ví dụ sau đây sẽ kiểm tra xem sự kiện này có đến từ TouchEvent
hay không bằng cách kiểm tra xem targetTouches
có tồn tại hay không. Nếu có, thì lớp này sẽ trích xuất clientX
và clientY
từ lần chạm đầu tiên.
Nếu sự kiện là PointerEvent
hoặc MouseEvent
, thì sự kiện đó sẽ trích xuất clientX
và clientY
trực tiếp từ chính sự kiện đó.
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
có 3 danh sách chứa dữ liệu chạm:
touches
: danh sách tất cả các thao tác chạm hiện tại trên màn hình, bất kể các thao tác đó đang ở phần tử DOM nào.targetTouches
: danh sách các lượt chạm hiện đang nằm trên phần tử DOM mà sự kiện được liên kết.changedTouches
: danh sách các lần chạm đã thay đổi dẫn đến sự kiện được kích hoạt.
Trong hầu hết các trường hợp, targetTouches
cung cấp cho bạn mọi thứ bạn cần và muốn. (Để biết thêm thông tin về các danh sách này, hãy xem Danh sách cảm ứng).
Sử dụng requestAnimationFrame
Vì các lệnh gọi lại sự kiện được kích hoạt trên luồng chính, nên chúng ta muốn chạy càng ít mã càng tốt trong lệnh gọi lại cho sự kiện, duy trì tốc độ khung hình cao và ngăn hiện tượng giật.
Khi sử dụng requestAnimationFrame()
, chúng ta có cơ hội cập nhật giao diện người dùng ngay trước khi trình duyệt dự định vẽ một khung và sẽ giúp chúng ta di chuyển một số công việc ra khỏi lệnh gọi lại sự kiện.
Nếu chưa quen với requestAnimationFrame()
, bạn có thể tìm hiểu thêm tại đây.
Cách triển khai thông thường là lưu toạ độ x
và y
từ sự kiện bắt đầu và di chuyển, đồng thời yêu cầu một khung ảnh động bên trong lệnh gọi lại sự kiện di chuyển.
Trong bản minh hoạ, chúng ta lưu trữ vị trí chạm ban đầu trong handleGestureStart()
(tìm 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);
Phương thức handleGestureMove()
lưu trữ vị trí của sự kiện trước khi yêu cầu một khung ảnh động nếu cần, truyền hàm onAnimFrame()
làm lệnh gọi lại:
this.handleGestureMove = function (evt) {
evt.preventDefault();
if (!initialTouchPos) {
return;
}
lastTouchPos = getGesturePointFromEvent(evt);
if (rafPending) {
return;
}
rafPending = true;
window.requestAnimFrame(onAnimFrame);
}.bind(this);
Giá trị onAnimFrame
là một hàm mà khi được gọi, sẽ thay đổi giao diện người dùng để di chuyển hàm đó. Bằng cách truyền hàm này vào requestAnimationFrame()
, chúng ta sẽ yêu cầu trình duyệt gọi hàm này ngay trước khi cập nhật trang (tức là vẽ mọi thay đổi đối với trang).
Trong lệnh gọi lại handleGestureMove()
, ban đầu chúng ta kiểm tra xem rafPending
có phải là sai hay không, cho biết liệu requestAnimationFrame()
có gọi onAnimFrame()
kể từ sự kiện di chuyển gần đây nhất hay không. Điều này có nghĩa là chúng ta chỉ có một requestAnimationFrame()
đang chờ chạy tại một thời điểm bất kỳ.
Khi lệnh gọi lại onAnimFrame()
được thực thi, chúng ta đặt phép biến đổi trên mọi phần tử mà chúng ta muốn di chuyển trước khi cập nhật rafPending
thành false
, cho phép sự kiện chạm tiếp theo yêu cầu một khung ảnh động mới.
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;
}
Điều khiển cử chỉ bằng thao tác chạm
Thuộc tính CSS touch-action
cho phép bạn kiểm soát hành vi chạm mặc định của một phần tử. Trong các ví dụ, chúng ta sử dụng touch-action: none
để ngăn trình duyệt thực hiện bất kỳ thao tác nào khi người dùng chạm vào, cho phép chúng ta chặn tất cả sự kiện chạm.
/* Pass all touches to javascript: */
button.custom-touch-logic {
touch-action: none;
}
Việc sử dụng touch-action: none
có phần giống như một lựa chọn hạt nhân vì nó ngăn tất cả các hành vi mặc định của trình duyệt. Trong nhiều trường hợp, một trong các tuỳ chọn bên dưới sẽ là giải pháp tốt hơn.
touch-action
cho phép bạn tắt các cử chỉ do trình duyệt triển khai.
Ví dụ: IE10 trở lên hỗ trợ cử chỉ nhấn đúp để thu phóng. Bằng cách đặt touch-action
là manipulation
, bạn sẽ ngăn chặn hành vi nhấn đúp mặc định.
Điều này cho phép bạn tự triển khai cử chỉ nhấn đúp.
Dưới đây là danh sách các giá trị touch-action
thường dùng:
Hỗ trợ các phiên bản IE cũ
Nếu muốn hỗ trợ IE10, bạn cần xử lý các phiên bản có tiền tố từ nhà cung cấp của PointerEvents
.
Để kiểm tra khả năng hỗ trợ PointerEvents
, bạn thường tìm window.PointerEvent
, nhưng trong IE10, bạn sẽ tìm window.navigator.msPointerEnabled
.
Tên sự kiện có tiền tố của nhà cung cấp là: 'MSPointerDown'
, 'MSPointerUp'
và 'MSPointerMove'
.
Ví dụ bên dưới cho bạn biết cách kiểm tra tính năng hỗ trợ và chuyển đổi tên sự kiện.
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;
}
Để biết thêm thông tin, hãy xem bài viết cập nhật này của Microsoft.
Tài liệu tham khảo
Lớp giả lập cho trạng thái chạm
Bạn có thể xem tài liệu tham khảo chính thức về các sự kiện chạm tại đây: Sự kiện chạm W3C.
Sự kiện chạm, chuột và con trỏ
Các sự kiện này là những thành phần cơ bản để thêm cử chỉ mới vào ứng dụng:
Danh sách cảm ứng
Mỗi sự kiện chạm bao gồm 3 thuộc tính danh sách:
Bật tính năng hỗ trợ trạng thái đang hoạt động trên iOS
Rất tiếc, Safari trên iOS không áp dụng trạng thái đang hoạt động theo mặc định. Để làm cho trạng thái này hoạt động, bạn cần thêm trình nghe sự kiện touchstart
vào phần nội dung tài liệu hoặc vào từng phần tử.
Bạn nên thực hiện việc này sau khi kiểm thử tác nhân người dùng để chỉ chạy trên thiết bị iOS.
Việc thêm điểm bắt đầu chạm vào phần thân có ưu điểm là áp dụng cho tất cả các phần tử trong DOM, tuy nhiên, điều này có thể gây ra vấn đề về hiệu suất khi cuộn trang.
window.onload = function() {
if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
document.body.addEventListener('touchstart', function() {}, false);
}
};
Giải pháp thay thế là thêm trình nghe bắt đầu chạm vào tất cả các phần tử có thể tương tác trong trang, giúp giảm bớt một số vấn đề về hiệu suất.
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);
}
}
};