Przejdź do treści głównej
Wersja: 0.80

Natywne komponenty interfejsu użytkownika w systemie Android

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 →

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 →

informacja

Native Module i Native Components to nasze stabilne technologie używane w starszej architekturze. Zostaną one wycofane w przyszłości, gdy Nowa Architektura stanie się stabilna. Nowa Architektura wykorzystuje Turbo Native Module i Fabric Native Components, aby osiągnąć podobne rezultaty.

Istnieje mnóstwo natywnych widżetów UI gotowych do użycia w nowoczesnych aplikacjach - część z nich jest elementem platformy, inne dostępne są jako biblioteki stron trzecich, a jeszcze inne mogą być używane w twoim własnym portfolio. React Native zawiera opakowane wersje kilku kluczowych komponentów platformy, takich jak ScrollView czy TextInput, ale nie wszystkich i z pewnością nie tych, które mogłeś stworzyć samodzielnie do poprzedniej aplikacji. Na szczęście możemy opakować te istniejące komponenty dla płynnej integracji z twoją aplikacją React Native.

Podobnie jak przewodnik po modułach natywnych, jest to bardziej zaawansowany przewodnik zakładający, że masz pewną znajomość programowania w Android SDK. Ten przewodnik pokaże ci, jak zbudować natywny komponent UI, przeprowadzając cię przez implementację podzbioru istniejącego komponentu ImageView dostępnego w głównej bibliotece React Native.

informacja

Możesz również skonfigurować lokalną bibliotekę zawierającą natywny komponent za pomocą jednego polecenia. Więcej szczegółów znajdziesz w przewodniku Konfiguracja bibliotek lokalnych.

Przykład ImageView

W tym przykładzie prześledzimy wymagania implementacyjne umożliwiające używanie komponentów ImageView w JavaScript.

Natywne widoki są tworzone i zarządzane przez rozszerzenie ViewManager lub częściej SimpleViewManager. SimpleViewManager jest w tym przypadku wygodny, ponieważ stosuje wspólne właściwości jak kolor tła, przezroczystość i układ Flexbox.

Te podklasy są w zasadzie singletonami - mostek tworzy tylko jedną instancję każdej z nich. Wysyłają natywne widoki do NativeViewHierarchyManager, który deleguje do nich ustawianie i aktualizację właściwości widoków w razie potrzeby. ViewManagers są również typowo delegatami dla widoków, wysyłając zdarzenia z powrotem do JavaScript poprzez mostek.

Aby wysłać widok:

  1. Utwórz podklasę ViewManager.

  2. Zaimplementuj metodę createViewInstance

  3. Udostępnij settery właściwości widoku przy użyciu adnotacji @ReactProp (lub @ReactPropGroup)

  4. Zarejestruj menedżer w createViewManagers pakietu aplikacji

  5. Zaimplementuj moduł JavaScript

1. Utwórz podklasę ViewManager

W tym przykładzie tworzymy klasę menedżera widoków ReactImageManager, która rozszerza SimpleViewManager typu ReactImageView. ReactImageView to typ obiektu zarządzanego przez menedżer, który będzie niestandardowym natywnym widokiem. Nazwa zwracana przez getName służy do odwoływania się do typu natywnego widoku z poziomu JavaScript.

java
public class ReactImageManager extends SimpleViewManager<ReactImageView> {

public static final String REACT_CLASS = "RCTImageView";
ReactApplicationContext mCallerContext;

public ReactImageManager(ReactApplicationContext reactContext) {
mCallerContext = reactContext;
}

@Override
public String getName() {
return REACT_CLASS;
}
}

2. Zaimplementuj metodę createViewInstance

Widoki są tworzone w metodzie createViewInstance. Widok powinien zainicjować się w stanie domyślnym, a wszelkie właściwości zostaną ustawione przez kolejne wywołanie updateView.

java
  @Override
public ReactImageView createViewInstance(ThemedReactContext context) {
return new ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, mCallerContext);
}

3. Udostępnij settery właściwości widoku przy użyciu adnotacji @ReactProp (lub @ReactPropGroup)

Właściwości, które mają być odzwierciedlone w JavaScript, muszą być udostępnione jako metody setter opatrzone adnotacją @ReactProp (lub @ReactPropGroup). Metoda setter powinna przyjmować jako pierwszy argument widok do aktualizacji (bieżącego typu widoku), a jako drugi argument wartość właściwości. Setter powinien być publiczny i nie zwracać wartości (tj. typ zwracany powinien być void w Javie lub Unit w Kotlinie). Typ właściwości wysyłany do JS jest określany automatycznie na podstawie typu argumentu wartości settera. Obecnie obsługiwane są następujące typy wartości (w Javie): boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap. Odpowiednie typy w Kotlinie to: Boolean, Int, Float, Double, String, ReadableArray, ReadableMap.

Adnotacja @ReactProp ma jeden obowiązkowy argument name typu String. Nazwa przypisana do adnotacji @ReactProp powiązanej z metodą setter jest używana do odwoływania się do właściwości po stronie JS.

Oprócz name, adnotacja @ReactProp może przyjmować następujące opcjonalne argumenty: defaultBoolean, defaultInt, defaultFloat. Argumenty te powinny być odpowiedniego typu (odpowiednio boolean, int, float w Javie oraz Boolean, Int, Float w Kotlinie), a podana wartość zostanie przekazana do metody settera w przypadku usunięcia właściwości, do której odnosi się setter. Wartości domyślne są dostępne tylko dla typów prostych – dla setterów obsługujących typy złożone, w przypadku usunięcia właściwości zostanie przekazana wartość null.

Wymagania dotyczące deklaracji setterów dla metod oznaczonych @ReactPropGroup różnią się od tych dla @ReactProp – szczegółowe informacje znajdują się w dokumentacji klasy adnotacji @ReactPropGroup. WAŻNE! W ReactJS aktualizacja wartości właściwości wywołuje metodę settera. Jedną z metod aktualizacji komponentu jest usuwanie wcześniej ustawionych właściwości. W takim przypadku setter również zostanie wywołany, aby powiadomić menedżera widoków o zmianie. Dla typów prostych zostanie przekazana wartość domyślna (określona przez argumenty defaultBoolean, defaultFloat itp. w adnotacji @ReactProp), natomiast dla typów złożonych setter otrzyma wartość null.

java
  @ReactProp(name = "src")
public void setSrc(ReactImageView view, @Nullable ReadableArray sources) {
view.setSource(sources);
}

@ReactProp(name = "borderRadius", defaultFloat = 0f)
public void setBorderRadius(ReactImageView view, float borderRadius) {
view.setBorderRadius(borderRadius);
}

@ReactProp(name = ViewProps.RESIZE_MODE)
public void setResizeMode(ReactImageView view, @Nullable String resizeMode) {
view.setScaleType(ImageResizeMode.toScaleType(resizeMode));
}

4. Rejestracja ViewManager

Ostatnim krokiem jest zarejestrowanie ViewManager w aplikacji, analogicznie do modułów natywnych, poprzez funkcję createViewManagers w pakiecie aplikacji.

java
  @Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new ReactImageManager(reactContext)
);
}

5. Implementacja modułu JavaScript

Końcowym etapem jest utworzenie modułu JavaScript definiującego warstwę interfejsu między Javą/Kotlin a JavaScript dla użytkowników nowego widoku. Zaleca się udokumentowanie interfejsu komponentu w tym module (np. przy użyciu TypeScript, Flow lub standardowych komentarzy).

ImageView.tsx
import {requireNativeComponent} from 'react-native';

/**
* Composes `View`.
*
* - src: Array<{url: string}>
* - borderRadius: number
* - resizeMode: 'cover' | 'contain' | 'stretch'
*/
export default requireNativeComponent('RCTImageView');

Funkcja requireNativeComponent przyjmuje nazwę widoku natywnego. Jeśli komponent wymaga zaawansowanej funkcjonalności (np. niestandardowej obsługi zdarzeń), należy opakować go w dodatkowy komponent Reacta, jak pokazano w przykładzie MyCustomView poniżej.

Zdarzenia

Wiemy już, jak udostępniać natywne komponenty widoków sterowane z poziomu JS, ale jak obsługiwać zdarzenia użytkownika takie jak powiększanie czy przesuwanie? Gdy wystąpi zdarzenie natywne, kod powinien wyemitować je do reprezentacji JavaScript widoku, powiązanej poprzez wartość zwracaną przez getId().

java
class MyCustomView extends View {
...
public void onReceiveNativeEvent() {
WritableMap event = Arguments.createMap();
event.putString("message", "MyMessage");
ReactContext reactContext = (ReactContext)getContext();
reactContext
.getJSModule(RCTEventEmitter.class)
.receiveEvent(getId(), "topChange", event);
}
}

Aby zmapować zdarzenie topChange na właściwość callback onChange w JavaScripcie, zarejestruj je poprzez nadpisanie metody getExportedCustomBubblingEventTypeConstants w ViewManager:

java
public class ReactImageManager extends SimpleViewManager<MyCustomView> {
...
public Map getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.builder().put(
"topChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange")
)
).build();
}
}

Callback otrzymuje surowe zdarzenie, które zwykle przetwarzamy w komponencie opakowującym dla uproszczenia API:

MyCustomView.tsx
import {useCallback} from 'react';
import {requireNativeComponent} from 'react-native';

const RCTMyCustomView = requireNativeComponent('RCTMyCustomView');

export default function MyCustomView(props: {
// ...
/**
* Callback that is called continuously when the user is dragging the map.
*/
onChangeMessage: (message: string) => unknown;
}) {
const onChange = useCallback(
event => {
props.onChangeMessage?.(event.nativeEvent.message);
},
[props.onChangeMessage],
);

return <RCTMyCustomView {...props} onChange={onChange} />;
}

Przykład integracji z fragmentem Androida

Aby zintegrować istniejące natywne elementy UI z aplikacją React Native, czasami potrzebna jest większa kontrola niż zwykłe zwracanie View z ViewManager. Fragmenty Androida umożliwiają wykorzystanie metod cyklu życia takich jak onViewCreated, onPause czy onResume. Poniższe kroki demonstrują to podejście:

1. Tworzenie przykładowego widoku niestandardowego

Najpierw utwórz klasę CustomView dziedziczącą po FrameLayout (zawartość może być dowolnym widokiem do renderowania).

CustomView.java
// replace with your package
package com.mypackage;

import android.content.Context;
import android.graphics.Color;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;

public class CustomView extends FrameLayout {
public CustomView(@NonNull Context context) {
super(context);
// set padding and background color
this.setPadding(16,16,16,16);
this.setBackgroundColor(Color.parseColor("#5FD3F3"));

// add default text view
TextView text = new TextView(context);
text.setText("Welcome to Android Fragments with React Native.");
this.addView(text);
}
}

2. Tworzenie Fragment

MyFragment.java
// replace with your package
package com.mypackage;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;

// replace with your view's import
import com.mypackage.CustomView;

public class MyFragment extends Fragment {
CustomView customView;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
super.onCreateView(inflater, parent, savedInstanceState);
customView = new CustomView(this.getContext());
return customView; // this CustomView could be any view that you want to render
}

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// do any logic that should happen in an `onCreate` method, e.g:
// customView.onCreate(savedInstanceState);
}

@Override
public void onPause() {
super.onPause();
// do any logic that should happen in an `onPause` method
// e.g.: customView.onPause();
}

@Override
public void onResume() {
super.onResume();
// do any logic that should happen in an `onResume` method
// e.g.: customView.onResume();
}

@Override
public void onDestroy() {
super.onDestroy();
// do any logic that should happen in an `onDestroy` method
// e.g.: customView.onDestroy();
}
}

3. Tworzenie podklasy ViewManager

MyViewManager.java
// replace with your package
package com.mypackage;

import android.view.Choreographer;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ThemedReactContext;

import java.util.Map;

public class MyViewManager extends ViewGroupManager<FrameLayout> {

public static final String REACT_CLASS = "MyViewManager";
public final int COMMAND_CREATE = 1;
private int propWidth;
private int propHeight;

ReactApplicationContext reactContext;

public MyViewManager(ReactApplicationContext reactContext) {
this.reactContext = reactContext;
}

@Override
public String getName() {
return REACT_CLASS;
}

/**
* Return a FrameLayout which will later hold the Fragment
*/
@Override
public FrameLayout createViewInstance(ThemedReactContext reactContext) {
return new FrameLayout(reactContext);
}

/**
* Map the "create" command to an integer
*/
@Nullable
@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of("create", COMMAND_CREATE);
}

/**
* Handle "create" command (called from JS) and call createFragment method
*/
@Override
public void receiveCommand(
@NonNull FrameLayout root,
String commandId,
@Nullable ReadableArray args
) {
super.receiveCommand(root, commandId, args);
int reactNativeViewId = args.getInt(0);
int commandIdInt = Integer.parseInt(commandId);

switch (commandIdInt) {
case COMMAND_CREATE:
createFragment(root, reactNativeViewId);
break;
default: {}
}
}

@ReactPropGroup(names = {"width", "height"}, customType = "Style")
public void setStyle(FrameLayout view, int index, Integer value) {
if (index == 0) {
propWidth = value;
}

if (index == 1) {
propHeight = value;
}
}

/**
* Replace your React Native view with a custom fragment
*/
public void createFragment(FrameLayout root, int reactNativeViewId) {
ViewGroup parentView = (ViewGroup) root.findViewById(reactNativeViewId);
setupLayout(parentView);

final MyFragment myFragment = new MyFragment();
FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity();
activity.getSupportFragmentManager()
.beginTransaction()
.replace(reactNativeViewId, myFragment, String.valueOf(reactNativeViewId))
.commit();
}

public void setupLayout(View view) {
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
manuallyLayoutChildren(view);
view.getViewTreeObserver().dispatchOnGlobalLayout();
Choreographer.getInstance().postFrameCallback(this);
}
});
}

/**
* Layout all children properly
*/
public void manuallyLayoutChildren(View view) {
// propWidth and propHeight coming from react-native props
int width = propWidth;
int height = propHeight;

view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY));

view.layout(0, 0, width, height);
}
}

4. Rejestracja ViewManager

MyPackage.java
// replace with your package
package com.mypackage;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.Arrays;
import java.util.List;

public class MyPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new MyViewManager(reactContext)
);
}

}

5. Rejestracja Package

MainApplication.java
@Override
protected List<ReactPackage> getPackages() {
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new MyAppPackage());
return packages;
}

6. Implementacja modułu JavaScript

I. Zacznij od niestandardowego menedżera widoków:

MyViewManager.tsx
import {requireNativeComponent} from 'react-native';

export const MyViewManager =
requireNativeComponent('MyViewManager');

II. Następnie zaimplementuj własny widok, wywołując metodę create:

MyView.tsx
import React, {useEffect, useRef} from 'react';
import {
PixelRatio,
UIManager,
findNodeHandle,
} from 'react-native';

import {MyViewManager} from './my-view-manager';

const createFragment = viewId =>
UIManager.dispatchViewManagerCommand(
viewId,
// we are calling the 'create' command
UIManager.MyViewManager.Commands.create.toString(),
[viewId],
);

export const MyView = () => {
const ref = useRef(null);

useEffect(() => {
const viewId = findNodeHandle(ref.current);
createFragment(viewId);
}, []);

return (
<MyViewManager
style={{
// converts dpi to px, provide desired height
height: PixelRatio.getPixelSizeForLayoutSize(200),
// converts dpi to px, provide desired width
width: PixelRatio.getPixelSizeForLayoutSize(200),
}}
ref={ref}
/>
);
};

Jeśli chcesz udostępnić settery właściwości za pomocą adnotacji @ReactProp (lub @ReactPropGroup), zobacz przykład ImageView powyżej.