Przejdź do treści głównej

Zdarzenia wskaźnikowe w React Native

· 9 minut czytania
Luna Wei
Luna Wei
Software Engineer @ Meta
Vincent Riemer
Vincent Riemer
Software Engineer @ Meta
Nieoficjalne Tłumaczenie Beta

Ta strona została przetłumaczona przez PageTurner AI (beta). Nie jest oficjalnie zatwierdzona przez projekt. Znalazłeś błąd? Zgłoś problem →

Dziś udostępniamy eksperymentalną, wieloplatformową API wskaźników dla React Native. Omówimy motywację, działanie oraz korzyści dla użytkowników. Znajdziecie też instrukcje włączania tej funkcji - z niecierpliwością czekamy na wasze opinie!

Minął ponad rok od przedstawienia naszej wizji wielu platform, która pokazywała zalety wykraczania poza mobilne i podnosiła poprzeczkę dla wszystkich systemów. W tym czasie zwiększyliśmy inwestycje w React Native dla VR, desktopu i webu. Różnice w sprzęcie i interakcjach na tych platformach skłoniły nas do przemyślenia, jak React Native powinien kompleksowo obsługiwać wprowadzanie danych.

Wykraczając poza dotyk

Desktop i VR tradycyjnie opierały się na myszy i klawiaturze, podczas gdy mobilne - głównie na dotyku. To podejście ewoluowało wraz z laptopami z ekranami dotykowymi i rosnącymi potrzebami obsługi klawiatury czy pióra na urządzeniach mobilnych. Obecny system zdarzeń dotykowych w React Native nie radzi sobie z tymi wyzwaniami.

W rezultacie użytkownicy platform spoza głównego drzewa forkowali React Native i/lub tworzyli własne komponenty natywne, by obsługiwać kluczowe funkcje jak wykrywanie najechania czy lewy przycisk myszy. To rozproszenie prowadzi do nadmiarowości właściwości z obsługą zdarzeń o podobnym celu, ale dla różnych platform. Zwiększa to złożoność frameworka i utrudnia współdzielenie kodu między platformami. Z tych powodów zespół postanowił dostarczyć wieloplatformową API wskaźników.

React Native dąży do dostarczania solidnych i ekspresyjnych API do budowania aplikacji na wiele platform przy zachowaniu charakterystycznych doświadczeń platformowych. Zaprojektowanie takiego API jest wyzwaniem, ale na szczęście istnieją wcześniejsze rozwiązania w obszarze wskaźników, z których React Native może skorzystać.

Czerpiąc z Webu

Web to platforma z podobnymi wyzwaniami skalowania na wiele systemów przy jednoczesnym uwzględnianiu przyszłościowego projektu. Konsorcjum World Wide Web (W3C) odpowiada za ustalanie standardów i propozycji budowy sieci współdziałającej między różnymi platformami i przeglądarkami.

Najbardziej istotne dla naszych potrzeb, W3C zdefiniowało zachowanie abstrakcyjnej formy wprowadzania danych zwanej wskaźnikiem. Specyfikacja Pointer Events rozszerza zdarzenia myszy i ma dostarczać jednolity zestaw zdarzeń i interfejsów dla wprowadzania danych z różnych urządzeń, pozwalając jednocześnie na obsługę specyficzną dla danego urządzenia gdy to konieczne.

Implementacja specyfikacji Pointer Events przynosi użytkownikom React Native wiele korzyści. Poza rozwiązaniem wspomnianych problemów, podnosi możliwości platform, które historycznie nie musiały obsługiwać wielu typów wprowadzania danych. Pomyśl o podłączaniu myszy Bluetooth do telefonu z Androidem czy obsłudze najechania przez Apple Pencil na iPadzie M2.

Zgodność ze specyfikacją daje też możliwość wymiany wiedzy między Webem a React Native. Edukacja o oczekiwaniach Weba dotyczących Pointer Events może jednocześnie służyć developerom React Native. Rozumiemy jednak, że wymagania React Native różnią się od webowych, a nasze podejście do specyfikacji to "najlepszy wysiłek" z dobrze udokumentowanymi odstępstwami dla jasności oczekiwań. Trwają prace nad dostosowaniem niektórych standardów webowych, by zmniejszyć fragmentację API w obszarach dostępności i wydajności.

Przenoszenie testów platformowych Weba

Choć specyfikacja Pointer Events dostarcza interfejsy i opisy zachowań API, uznaliśmy, że nie są wystarczająco szczegółowe, by pewnie wprowadzać zmiany i powoływać się na specyfikację jako weryfikację. Przeglądarki używają jednak innego mechanizmu zapewniającego zgodność i interoperacyjność - testów platformowych Weba!

Testy platformowe Weba pisane są pod imperatywne API DOM przeglądarek - nieobsługiwane przez React Native, który używa własnych prymitywów widoków. Oznacza to, że nie możemy współdzielić tych testów z przeglądarkami i zamiast tego stworzyliśmy analogiczne API testowe dla React Native, ułatwiające przenoszenie testów platformowych Weba.

Wdrożyliśmy nowy framework testów manualnych, którego używamy do weryfikacji naszych implementacji przez RNTester. Testy te tymczasowo nazywamy RNTester Platform Tests i są wciąż dość podstawowe. Nasza implementacja dostarcza API do budowania przypadków testowych jako komponentów, które są renderowane, a wyniki raportowane wyłącznie przez interfejs użytkownika.

GIF przedstawiający porównanie testu "Pointer Events hoverable pointer attributes" działającego w React Native (iOS) po lewej stronie oraz w przeglądarce (oryginalna implementacja) po prawej.

Testy te będą nadal pomocne w miarę rozwijania naszej implementacji zdarzeń wskaźnikowych. Będą również skalować się do testowania implementacji na platformach innych niż Android i iOS. Wraz ze wzrostem liczby testów w naszym zestawie będziemy dążyć do zautomatyzowania ich uruchamiania, aby lepiej wykrywać regresje w implementacjach.

Jak to działa

Duża część naszej implementacji zdarzeń wskaźnikowych bazuje na istniejącej infrastrukturze do obsługi zdarzeń dotykowych. Na Androidzie i iOS wykorzystujemy odpowiednio zdarzenia MotionEvent i UITouch. Ogólny przepływ dystrybucji zdarzeń przedstawiono poniżej.

Diagram przepływu kodu interpretującego zdarzenia wejściowe z interfejsu użytkownika Androida/iOS na zdarzenia wskaźnikowe. Na Androidzie procedury obsługi "onTouchEvent" i "onHoverEvent" emitują "MotionEvents", które są interpretowane na zdarzenia wskaźnikowe i przez JSI przekazywane do renderera Reacta. iOS działa podobnie z procedurami "touchesBegan", "touchesMoved", "touchesEnded" i "hovering" interpretującymi "UITouch" i "UIEvent" na zdarzenia wskaźnikowe.

Na przykładzie Androida, ogólne podejście do wykorzystania zdarzeń platformowych to:

  1. Iteracja przez wszystkie wskaźniki MotionEvent i przeszukiwanie w głąb w celu określenia docelowego widoku Reacta dla każdego wskaźnika oraz jego ścieżki przodków.

  2. Mapowanie kategorii MotionEvent na odpowiednie zdarzenia wskaźnikowe. Istnieje relacja 1-do-wielu między MotionEvent a PointerEvent. Na diagramie ilustrującym ich zależność, przerywane linie oznaczają zdarzenia wywoływane, gdy urządzenie wskazujące nie obsługuje najeżdżania (hover).

Diagram ilustrujący relację typów MotionEvent Androida z wywoływanymi zdarzeniami wskaźnikowymi. Niektóre zdarzenia są wywoływane warunkowo, gdy urządzenie wskazujące nie obsługuje najeżdżania. "ACTION_DOWN" i "ACTION_POINTER_DOWN" wywołują pointerdown i warunkowo pointerenter, pointerover. "ACTION_MOVE" i "ACTION_HOVER_MOVE" wywołują pointerover, pointermove, pointerout, pointerup. "ACTION_UP" i "ACTION_POINTER_UP" wywołują pointerup i warunkowo pointerout, pointerleave.

  1. Budowanie interfejsu PointerEvent z wykorzystaniem szczegółów platformy z MotionEvent i stanu buforowanego z poprzednich interakcji (np. właściwość button).

  2. Przesyłanie zdarzeń wskaźnikowych z Androida do kolejki zdarzeń React Native i wykorzystanie JSI do wywołania metody dispatchEvent w react-native-renderer, która iteruje przez drzewo Reacta w fazie bąbelkowania i przechwytywania zdarzenia.

Postęp implementacji

Jeśli chodzi o obecny postęp implementacji specyfikacji zdarzeń wskaźnikowych, skupiliśmy się na solidnej implementacji podstawowej najczęstszych zdarzeń obsługujących naciskanie, najeżdżanie i przesuwanie.

Zdarzenia

ImplementedWork in ProgressYet to be Implemented
onPointerOveronPointerCancelonClick
onPointerEnteronContextMenu
onPointerDownonGotPointerCapture
onPointerMoveonLostPointerCapture
onPointerUponPointerRawUpdate
onPointerOut
onPointerLeave
informacja

onPointerCancel został podpięty pod natywne zdarzenie "cancel" platformy, ale niekoniecznie odpowiada to sytuacjom, w których platforma webowa oczekuje jego wywołania.

Właściwości zdarzeń

Dla każdego wspomnianego zdarzenia zaimplementowaliśmy większość właściwości oczekiwanych w obiekcie PointerEvent — choć w React Native są one dostępne przez właściwość event.nativeEvent. Pełną listę zaimplementowanych właściwości można znaleźć w definicji interfejsu Flowtype. Godnym uwagi wyjątkiem jest właściwość relatedTarget, której pełna implementacja jest nietrywialna ze względu na trudności w udostępnianiu natywnych referencji widoków w tym kontekście.

Prace przyszłościowe i eksploracje

Oprócz wymienionych powyżej zdarzeń istnieją również inne interfejsy API związane ze zdarzeniami wskaźnika (Pointer Events). W ramach tego projektu planujemy wdrożyć następujące funkcjonalności:

  • Pointer Capture API

    • Obejmuje imperatywne API dostępne poprzez referencje do elementów, w tym setPointerCapture(), releasePointerCapture() oraz hasPointerCapture().
  • Właściwość stylu touch-action

    • W środowisku webowym ta właściwość CSS służy do deklaratywnego negocjowania gestów między przeglądarką a kodem obsługi zdarzeń strony. W React Native może być wykorzystana do koordynacji obsługi zdarzeń między procedurami zdarzeń wskaźnika komponentu View a nadrzędnym ScrollView.
  • click, contextmenu, auxclick

    • click to abstrakcyjna definicja interakcji, która może być wywołana poprzez mechanizmy dostępności (accessibility) lub inne charakterystyczne dla platformy interakcje.

Kolejną zaletą natywnej implementacji zdarzeń wskaźnika jest możliwość usprawnienia obsługi gestów, obecnie ograniczonej tylko do zdarzeń dotykowych i realizowanej w JavaScript poprzez interfejsy Responder, Pressability i PanResponder.

Ponadto badamy możliwość wdrożenia interfejsu EventTarget dla hostowych komponentów React Native (tj. add/removeEventListener), co umożliwi tworzenie dodatkowych abstrakcji w kodzie użytkownika do obsługi interakcji wskaźnika.

Testowanie funkcjonalności

Nasza implementacja zdarzeń wskaźnika jest nadal w fazie eksperymentalnej, ale zależy nam na opinii społeczności. Aby przetestować to API, należy włączyć kilka flag funkcjonalności:

Włączanie flag funkcjonalności

niebezpieczeństwo

Nadpisywanie natywnych flag funkcji poniżej (takich jak RCTConstants i ReactFeatureFlags) sięga technicznie do wewnętrznych mechanizmów React Native. Takie działanie może wkrótce zakłócić Twoją konfigurację, ponieważ pracujemy nad ich wycofaniem w celu szerszego wdrożenia obsługi zdarzeń wskaźnikowych.

uwaga

Zdarzenia wskaźnika są dostępne tylko w Nowej Architekturze (Fabric) i wymagają React Native 0.71+, który w momencie pisania tego tekstu jest wersją release candidate.

W głównym pliku JavaScript (index.js w domyślnym szablonie React Native) włącz flagę shouldEmitW3CPointerEvents dla zdarzeń wskaźnika oraz shouldPressibilityUseW3CPointerEventsForHover do integracji z Pressability.

import ReactNativeFeatureFlags from 'react-native/Libraries/ReactNative/ReactNativeFeatureFlags';

// enable the JS-side of the w3c PointerEvent implementation
ReactNativeFeatureFlags.shouldEmitW3CPointerEvents = () => true;

// enable hover events in Pressibility to be backed by the PointerEvent implementation
ReactNativeFeatureFlags.shouldPressibilityUseW3CPointerEventsForHover =
() => true;

Konfiguracja dla iOS

Aby zapewnić wysyłanie zdarzeń z natywnego renderera iOS, włącz odpowiednią flagę w kodzie inicjalizacyjnym aplikacji (zwykle w pliku AppDelegate.mm).

#import <React/RCTConstants.h>

// ...

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTSetDispatchW3CPointerEvents(YES);

// ...
}

Aby implementacja rozróżniała wskaźniki myszy i dotyku na iOS, dodaj UIApplicationSupportsIndirectInputEvents do pliku info.plist projektu w Xcode.

Konfiguracja dla Androida

Podobnie jak w iOS, włącz flagę funkcjonalności w kodzie inicjalizacyjnym aplikacji Androida (zwykle w metodzie onCreate głównej aktywności React lub surface).

import com.facebook.react.config.ReactFeatureFlags;

//... somewhere in initialization

@Override
public void onCreate() {
ReactFeatureFlags.dispatchPointerEvents = true;
}

JavaScript

function onPointerOver(event) {
console.log(
'Over blue box offset: ',
event.nativeEvent.offsetX,
event.nativeEvent.offsetY,
);
}

// ... in some component
<View
onPointerOver={onPointerOver}
style={{height: 100, width: 100, backgroundColor: 'blue'}}
/>;

Zapraszamy do przekazywania opinii

Obecnie zdarzenia wskaźnika zasilają naszą platformę VR i sklep Oculus, ale zależy nam na wczesnych opiniach społeczności dotyczących zarówno podejścia, jak i implementacji. Jeśli masz pytania lub przemyślenia, dołącz do dedykowanej dyskusji o zdarzeniach wskaźnika.