Màn hình cảm ứng có trên ngày càng 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 lại thao tác chạm của người dùng theo cách trực quan và thú vị.
Màn hình cảm ứng có trên ngày càng nhiều thiết bị, từ điện thoại cho đế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 với thao tác chạm của họ theo cách trực quan.
Phản hồi trạng thái của thành phần
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 phần tử 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 của bạn, bạn sẽ cảm thấy yên tâm rằng trang web của bạn đang hoạt động. Điều này không chỉ giúp giảm sự thất vọng mà còn mang lại cảm giác phản hồi nhanh gọn.
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, tâm đ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 trong số này, chúng ta cần áp dụng kiểu cho các lớp giả sau đây :hover
, :focus
và :active
như thể hiện dưới đây:
.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 dành cho thiết bị di động, các trạng thái hover và/hoặc hover sẽ áp dụng cho một phần tử sau khi bạn nhấn vào phần tử đó.
Hãy cân nhắc kỹ những kiểu bạn đã đặt và giao diện mà người dùng sẽ thấy 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 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 kiểu riêng để phản hồi thao tác chạm của người dùng. Nguyên nhân chủ yếu là do khi thiết bị di động ra mắt lần đầu tiên, một số trang web không tạo kiểu cho trạng thái :active
. Do đó, nhiều trình duyệt đã thêm kiểu hoặc màu đánh dấu bổ sung để cung cấp ý kiến 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ị vòng tròn xung quanh một phần tử khi một phần tử được lấy làm tâm điểm. Bạn có thể chặn bằng:
.btn:focus {
outline: 0;
/* Add replacement focus styling here (i.e. border) */
}
Safari và Chrome thêm một màu đánh dấu thao tác nhấn mà bạn có thể ngăn chặn 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ó hành vi tương tự, nhưng bị chặn qua thẻ meta:
<meta name="msapplication-tap-highlight" content="no">
Firefox có hai hiệu ứng phụ cần xử lý.
Lớp giả -moz-focus-inner
thêm đường viền trên các phần tử có thể chạm vào, bạn có thể xoá bằng cách đặt border: 0
.
Nếu đang sử dụng phần tử <button>
trên Firefox, bạn sẽ được áp dụng một hiệu ứng chuyển màu (gradient) mà bạn có thể xoá 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 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 thành phần nhưng lại muốn ngăn chặ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 với thuộc tính CSS user-select
, nhưng xin lưu ý rằng việc này đối với nội dung có thể extremely gây khó chịu cho người dùng nếu họ muốn chọn văn bản trong phần tử.
Vì vậy, hãy đảm bảo bạn sử dụng mã này một cách thận trọng và thận trọng.
/* 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 hành động tương tác và cử chỉ tuỳ chỉnh cho trang web của mình, thì có 2 chủ đề cần lưu ý:
- Cách hỗ trợ mọi 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 những chủ đề bao gồm API mà chúng ta cần hỗ trợ để đáp ứng tất cả trình duyệt, sau đó đề cập đến cách chúng ta sử dụng những sự kiện này một cách hiệu quả.
Tuỳ thuộc vào thao tác mà bạn muốn thực hiện cử chỉ, 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 lúc.
Chúng ta sẽ xem xét 2 ví dụ trong bài viết này, cả hai đều minh hoạ khả năng hỗ trợ cho mọi 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 cấp tất cả sự kiện chạm 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 cao cho người dùng, nhưng hạn chế 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 muốn người dùng tương tác với nhiều thành phần cùng một lúc (sử dụng tính năng cảm ứng đa điểm), thì bạn nên hạn chế thao tác chạm đối với thành phần cụ thể đó.
Phương thức 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à khó chống chọi với 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 đúng.
Tính năng tuyệt vời của PointerEvents
là hợp nhất nhiều loại phương thức nhập, bao gồm cả các sự kiện nhấp chuột, chạm và bút thành một tập hợp các lệnh gọi lại. Các sự kiện cần theo dõi là pointerdown
, pointermove
, pointerup
và pointercancel
.
Các trình duyệt 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. Nếu muốn triển khai cùng một cử chỉ để 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 nên sử dụng, hãy xem bảng các sự kiện cho thao tác chạm, chuột và con trỏ.
Để sử dụng những sự kiện này, bạn phải gọi phương thức addEventListener()
trên phần tử DOM, cùng với tên của sự kiện, hàm callback và giá trị boolean.
Boolean xác định xem bạn nên nắm bắt sự kiện trước hay sau khi các phần tử khác có cơ hội nắm bắt và diễn giải các sự kiện đó. (true
có nghĩa là bạn muốn sự kiện xuất hiện trước các phần tử khác.)
Dưới đây là ví dụ về cách lắng nghe khi 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ý tương tác một phần tử
Trong đoạn mã ngắn ở trên, chúng tôi 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 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 cử chỉ bắt đầu bất kể vị trí xảy ra thao tác chạm và PointerEvents
sẽ theo dõi các sự kiện bất kể vị trí xảy ra thao tác chạm sau khi chúng ta gọi setPointerCapture
trên phần tử DOM.
Đối với các sự kiện kết thúc và sự kiện di chuyển chuột, chúng tôi 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 cần thực hiện để triển khai việc này là:
- Thêm tất cả trình nghe TouchEvent và PointerEvent. Đối với MouseEvents, chỉ thêm sự kiện bắt đầu.
- Bên trong lệnh gọi lại cử chỉ bắt đầu, 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, bạn sẽ nhận được mọi sự kiện chuột bất kể sự kiện đó có xảy ra trên phần tử gốc 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 khác. Sau đó, xử lý điểm bắt đầu cử chỉ. - Xử lý các sự kiện di chuyển.
- Ở sự kiện kết thúc, hãy xoá trình nghe kết thúc và di chuyển chuột khỏi tài liệu rồi kết thúc cử chỉ.
Dưới đây là một đoạn mã của phương thức handleGestureStart()
. Đoạn mã 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 gọi lại 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 giải phóng thao tác 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 thêm sự kiện di chuyển này 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 bên ngoài phần tử đó, thì chúng ta sẽ tiếp tục nhận được thao tác di chuyển chuột bất kể chúng đang ở đâu trên trang vì tài liệu đang nhận các sự kiện.
Sơ đồ này cho thấy hoạt động của các sự kiện chạm 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 hiệu quả
Giờ đây, khi đã xử lý các 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 mọi sự kiện bắt đầu và di chuyển, 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 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ì công cụ sẽ trích xuất clientX
và clientY
ngay từ lần chạm đầu tiên.
Nếu sự kiện là PointerEvent
hoặc MouseEvent
, thì 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 cảm ứng:
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 bật phần tử DOM nào.targetTouches
: danh sách các thao tác chạm hiện tại trên phần tử DOM mà sự kiện liên kết với.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 trường hợp, targetTouches
sẽ cung cấp cho bạn mọi thứ bạn cần và mong muốn. (Để biết thêm thông tin về các danh sách này, hãy xem phần 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 cần chạy càng ít mã càng tốt trong các lệnh gọi lại cho các sự kiện của mình, giữ cho tốc độ khung hình cao và ngăn hiện tượng giật.
Bằng cách 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 có ý định vẽ một khung và sẽ giúp di chuyển một số công việc khỏi các 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 điển hình là lưu các toạ độ x
và y
từ các 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 khung ảnh động nếu cần, truyền vào hàm
onAnimFrame()
dưới dạng 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 giao diện người dùng xung quanh. Khi chuyể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 trên trang).
Trong lệnh gọi lại handleGestureMove()
, ban đầu chúng tôi kiểm tra xem rafPending
có phải là sai hay không. Điều này cho biết liệu onAnimFrame()
có được requestAnimationFrame()
gọi 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 tôi chỉ có một requestAnimationFrame()
đang chờ để chạy tại một thời điểm bất kỳ.
Khi thực thi lệnh gọi lại onAnimFrame()
, 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 ví dụ này, chúng ta sử dụng touch-action: none
để ngăn trình duyệt thực hiện bất kỳ hành động nào với thao tác chạm của người dùng, cho phép chúng ta chặn tất cả cá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 không phù hợp vì lựa chọn này ngăn chặ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, bạn nên chọn một trong các phương án dưới đây.
touch-action
cho phép bạn tắt các cử chỉ do trình duyệt triển khai.
Ví dụ: IE10+ 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.
Thao tác 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 cũ hơn của IE
Nếu muốn hỗ trợ IE10, bạn cần phải xử lý các phiên bản PointerEvents
có tiền tố nhà cung cấp.
Để kiểm tra khả năng hỗ trợ của PointerEvents
, thường thì bạn sẽ 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ố 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 khả 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 này về bản cập nhật của Microsoft.
Tài liệu tham khảo
Lớp giả cho trạng thái cảm ứng
Bạn có thể xem tài liệu tham khảo về sự kiện chạm cuối cùng tại đây: Sự kiện chạm W3C.
Các sự kiện chạm, chuột và con trỏ
Những sự kiện này là yếu tố nền tảng để thêm các cử chỉ mới vào ứng dụng của bạn:
Danh sách cảm ứng
Mỗi sự kiện chạm bao gồm ba 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. Để ứng dụng 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 cho từng phần tử.
Bạn nên thực hiện việc này sau một bài kiểm thử tác nhân người dùng để mã này chỉ chạy trên thiết bị iOS.
Việc thêm bắt đầu chạm vào phần nội dung có lợi thế là áp dụng cho tất cả các phần tử trong DOM. Tuy nhiên, việc 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);
}
};
Một cách khác là thêm trình nghe bắt đầu thao tác chạm vào tất cả các phần tử có thể tương tác trên 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);
}
}
};