Componentes de interfaz de usuario nativos de Android
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Esta página fue traducida por PageTurner AI (beta). No está respaldada oficialmente por el proyecto. ¿Encontraste un error? Reportar problema →
Native Module y Native Components son nuestras tecnologías estables utilizadas por la arquitectura heredada. Serán desaprobadas en el futuro cuando la Nueva Arquitectura sea estable. La Nueva Arquitectura utiliza Turbo Native Module y Fabric Native Components para lograr resultados similares.
Existen una gran cantidad de componentes de interfaz de usuario nativos listos para usarse en las aplicaciones más recientes: algunos forman parte de la plataforma, otros están disponibles como bibliotecas de terceros, y otros más podrían estar en uso en tu propio portafolio. React Native ya incluye varios de los componentes más críticos de la plataforma, como ScrollView y TextInput, pero no todos, y ciertamente no aquellos que podrías haber creado para una aplicación anterior. Afortunadamente, podemos encapsular estos componentes existentes para integrarlos perfectamente con tu aplicación de React Native.
Al igual que la guía de módulos nativos, este también es un documento avanzado que asume cierto conocimiento de programación con el SDK de Android. Esta guía te mostrará cómo crear un componente de UI nativo, guiándote a través de la implementación de un subconjunto del componente ImageView disponible en la biblioteca principal de React Native.
También puedes configurar una biblioteca local que contenga componentes nativos con un solo comando. Consulta la guía de Configuración de bibliotecas locales para más detalles.
Ejemplo de ImageView
Para este ejemplo, repasaremos los requisitos de implementación para permitir el uso de ImageViews en JavaScript.
Las vistas nativas se crean y manipulan extendiendo ViewManager o más comúnmente SimpleViewManager. Un SimpleViewManager es conveniente en este caso porque aplica propiedades comunes como color de fondo, opacidad y diseño Flexbox.
Estas subclases son esencialmente singletons: solo se crea una instancia de cada una mediante el bridge. Envían vistas nativas al NativeViewHierarchyManager, que les delega establecer y actualizar las propiedades de las vistas según sea necesario. Los ViewManagers también suelen actuar como delegados para las vistas, enviando eventos de vuelta a JavaScript a través del bridge.
Para enviar una vista:
-
Crea la subclase de ViewManager.
-
Implementa el método
createViewInstance -
Expón los setters de propiedades de la vista usando la anotación
@ReactProp(o@ReactPropGroup) -
Registra el administrador en
createViewManagersdel paquete de la aplicación. -
Implementa el módulo JavaScript
1. Crea la subclase ViewManager
En este ejemplo creamos la clase administradora de vistas ReactImageManager que extiende SimpleViewManager del tipo ReactImageView. ReactImageView es el tipo de objeto gestionado por el administrador, que será la vista nativa personalizada. El nombre devuelto por getName se usa para referenciar el tipo de vista nativa desde JavaScript.
- Java
- Kotlin
class ReactImageManager(
private val callerContext: ReactApplicationContext
) : SimpleViewManager<ReactImageView>() {
override fun getName() = REACT_CLASS
companion object {
const val REACT_CLASS = "RCTImageView"
}
}
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. Implementa el método createViewInstance
Las vistas se crean en el método createViewInstance. La vista debe inicializarse en su estado predeterminado; cualquier propiedad se establecerá mediante una llamada posterior a updateView.
- Java
- Kotlin
override fun createViewInstance(context: ThemedReactContext) =
ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, callerContext)
@Override
public ReactImageView createViewInstance(ThemedReactContext context) {
return new ReactImageView(context, Fresco.newDraweeControllerBuilder(), null, mCallerContext);
}
3. Expón setters de propiedades de la vista usando la anotación @ReactProp (o @ReactPropGroup)
Las propiedades que deben reflejarse en JavaScript deben exponerse como métodos setter anotados con @ReactProp (o @ReactPropGroup). El método setter debe tomar como primer argumento la vista a actualizar (del tipo de vista actual) y como segundo argumento el valor de la propiedad. El setter debe ser público y no devolver ningún valor (es decir, el tipo de retorno debe ser void en Java o Unit en Kotlin). El tipo de propiedad enviado a JS se determina automáticamente según el tipo del argumento de valor del setter. Actualmente se admiten los siguientes tipos de valores (en Java): boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap. Los tipos correspondientes en Kotlin son Boolean, Int, Float, Double, String, ReadableArray, ReadableMap.
La anotación @ReactProp tiene un argumento obligatorio name de tipo String. El nombre asignado a la anotación @ReactProp vinculada al método setter se utiliza para referenciar la propiedad en el lado de JS.
Además de name, la anotación @ReactProp puede tomar los siguientes argumentos opcionales: defaultBoolean, defaultInt, defaultFloat. Estos argumentos deben ser del tipo correspondiente (boolean, int, float en Java y Boolean, Int, Float en Kotlin). El valor proporcionado se pasará al método setter cuando la propiedad referenciada haya sido eliminada del componente. Nota: los valores "default" solo se proporcionan para tipos primitivos. Para setters de tipos complejos, se pasará null como valor predeterminado si la propiedad correspondiente se elimina.
Los requisitos de declaración para métodos anotados con @ReactPropGroup difieren de @ReactProp. Consulta la documentación de la clase @ReactPropGroup para más detalles. IMPORTANTE: En ReactJS, actualizar el valor de una propiedad ejecutará su método setter. Ten en cuenta que al eliminar propiedades previamente establecidas también se invocará el setter para notificar al administrador de vistas sobre el cambio. En este caso, se proporcionará el valor "default" (para tipos primitivos, este valor puede especificarse mediante los argumentos defaultBoolean, defaultFloat, etc. de la anotación @ReactProp; para tipos complejos el setter recibirá null).
- Java
- Kotlin
@ReactProp(name = "src")
fun setSrc(view: ReactImageView, sources: ReadableArray?) {
view.setSource(sources)
}
@ReactProp(name = "borderRadius", defaultFloat = 0f)
override fun setBorderRadius(view: ReactImageView, borderRadius: Float) {
view.setBorderRadius(borderRadius)
}
@ReactProp(name = ViewProps.RESIZE_MODE)
fun setResizeMode(view: ReactImageView, resizeMode: String?) {
view.setScaleType(ImageResizeMode.toScaleType(resizeMode))
}
@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. Registrar el ViewManager
El paso final es registrar el ViewManager en la aplicación, similar a cómo se registran los Módulos Nativos, mediante la función createViewManagers del paquete de la aplicación.
- Java
- Kotlin
override fun createViewManagers(
reactContext: ReactApplicationContext
) = listOf(ReactImageManager(reactContext))
@Override
public List<ViewManager> createViewManagers(
ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new ReactImageManager(reactContext)
);
}
5. Implementar el módulo JavaScript
El último paso es crear el módulo JavaScript que define la capa de interfaz entre Java/Kotlin y JavaScript para los usuarios de tu nueva vista. Se recomienda documentar la interfaz del componente en este módulo (usando TypeScript, Flow o comentarios simples).
import {requireNativeComponent} from 'react-native';
/**
* Composes `View`.
*
* - src: Array<{url: string}>
* - borderRadius: number
* - resizeMode: 'cover' | 'contain' | 'stretch'
*/
export default requireNativeComponent('RCTImageView');
La función requireNativeComponent toma el nombre de la vista nativa. Nota: si necesitas funcionalidades avanzadas (como manejo personalizado de eventos), envuelve el componente nativo en otro componente React. Esto se ilustra en el ejemplo MyCustomView a continuación.
Eventos
Ahora sabemos cómo exponer componentes nativos controlables desde JS, pero ¿cómo manejamos eventos del usuario como zoom o desplazamiento? Cuando ocurre un evento nativo, el código debe emitirlo a la representación JavaScript de la Vista. Ambos componentes se vinculan mediante el valor devuelto por getId().
- Java
- Kotlin
class MyCustomView(context: Context) : View(context) {
...
fun onReceiveNativeEvent() {
val event = Arguments.createMap().apply {
putString("message", "MyMessage")
}
val reactContext = context as ReactContext
reactContext
.getJSModule(RCTEventEmitter::class.java)
.receiveEvent(id, "topChange", event)
}
}
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);
}
}
Para mapear el nombre de evento topChange al prop onChange en JavaScript, regístralo sobrescribiendo getExportedCustomBubblingEventTypeConstants en tu ViewManager:
- Java
- Kotlin
class ReactImageManager : SimpleViewManager<MyCustomView>() {
...
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> {
return mapOf(
"topChange" to mapOf(
"phasedRegistrationNames" to mapOf(
"bubbled" to "onChange"
)
)
)
}
}
public class ReactImageManager extends SimpleViewManager<MyCustomView> {
...
public Map getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.builder().put(
"topChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange")
)
).build();
}
}
Esta callback recibe el evento en bruto, que normalmente procesamos en el componente wrapper para simplificar la API:
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} />;
}
Ejemplo de integración con un Fragmento de Android
Para integrar elementos nativos existentes en tu app de React Native, quizá necesites usar Fragmentos de Android. Esto ofrece mayor control granular que devolver una View desde tu ViewManager, especialmente si requieres lógica personalizada vinculada a métodos de ciclo de vida como onViewCreated, onPause o onResume. Sigue estos pasos:
1. Crear una vista personalizada de ejemplo
Primero, crea una clase CustomView que extienda FrameLayout (su contenido puede ser cualquier vista que desees renderizar).
- Java
- Kotlin
// replace with your package
package com.mypackage
import android.content.Context
import android.graphics.Color
import android.widget.FrameLayout
import android.widget.TextView
class CustomView(context: Context) : FrameLayout(context) {
init {
// set padding and background color
setPadding(16,16,16,16)
setBackgroundColor(Color.parseColor("#5FD3F3"))
// add default text view
addView(TextView(context).apply {
text = "Welcome to Android Fragments with React Native."
})
}
}
// 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. Crear un Fragment
- Java
- Kotlin
// 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
class MyFragment : Fragment() {
private lateinit var customView: CustomView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
customView = CustomView(requireNotNull(context))
return customView // this CustomView could be any view that you want to render
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// do any logic that should happen in an `onCreate` method, e.g:
// customView.onCreate(savedInstanceState);
}
override fun onPause() {
super.onPause()
// do any logic that should happen in an `onPause` method
// e.g.: customView.onPause();
}
override fun onResume() {
super.onResume()
// do any logic that should happen in an `onResume` method
// e.g.: customView.onResume();
}
override fun onDestroy() {
super.onDestroy()
// do any logic that should happen in an `onDestroy` method
// e.g.: customView.onDestroy();
}
}
// 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. Crear la subclase de ViewManager
- Java
- Kotlin
// 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.fragment.app.FragmentActivity
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.uimanager.annotations.ReactPropGroup
class MyViewManager(
private val reactContext: ReactApplicationContext
) : ViewGroupManager<FrameLayout>() {
private var propWidth: Int? = null
private var propHeight: Int? = null
override fun getName() = REACT_CLASS
/**
* Return a FrameLayout which will later hold the Fragment
*/
override fun createViewInstance(reactContext: ThemedReactContext) =
FrameLayout(reactContext)
/**
* Map the "create" command to an integer
*/
override fun getCommandsMap() = mapOf("create" to COMMAND_CREATE)
/**
* Handle "create" command (called from JS) and call createFragment method
*/
override fun receiveCommand(
root: FrameLayout,
commandId: String,
args: ReadableArray?
) {
super.receiveCommand(root, commandId, args)
val reactNativeViewId = requireNotNull(args).getInt(0)
when (commandId.toInt()) {
COMMAND_CREATE -> createFragment(root, reactNativeViewId)
}
}
@ReactPropGroup(names = ["width", "height"], customType = "Style")
fun setStyle(view: FrameLayout, index: Int, value: Int) {
if (index == 0) propWidth = value
if (index == 1) propHeight = value
}
/**
* Replace your React Native view with a custom fragment
*/
fun createFragment(root: FrameLayout, reactNativeViewId: Int) {
val parentView = root.findViewById<ViewGroup>(reactNativeViewId)
setupLayout(parentView)
val myFragment = MyFragment()
val activity = reactContext.currentActivity as FragmentActivity
activity.supportFragmentManager
.beginTransaction()
.replace(reactNativeViewId, myFragment, reactNativeViewId.toString())
.commit()
}
fun setupLayout(view: View) {
Choreographer.getInstance().postFrameCallback(object: Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
manuallyLayoutChildren(view)
view.viewTreeObserver.dispatchOnGlobalLayout()
Choreographer.getInstance().postFrameCallback(this)
}
})
}
/**
* Layout all children properly
*/
private fun manuallyLayoutChildren(view: View) {
// propWidth and propHeight coming from react-native props
val width = requireNotNull(propWidth)
val height = requireNotNull(propHeight)
view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY))
view.layout(0, 0, width, height)
}
companion object {
private const val REACT_CLASS = "MyViewManager"
private const val COMMAND_CREATE = 1
}
}
// 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. Registrar el ViewManager
- Java
- Kotlin
// replace with your package
package com.mypackage
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class MyPackage : ReactPackage {
...
override fun createViewManagers(
reactContext: ReactApplicationContext
) = listOf(MyViewManager(reactContext))
}
// 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. Registrar el Package
- Java
- Kotlin
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(MyAppPackage())
}
@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. Implementar el módulo JavaScript
I. Comienza con el administrador de vistas personalizado:
import {requireNativeComponent} from 'react-native';
export const MyViewManager =
requireNativeComponent('MyViewManager');
II. Luego implementa la Vista personalizada llamando al método create:
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}
/>
);
};
Si deseas exponer establecedores de propiedades usando la anotación @ReactProp (o @ReactPropGroup), consulta el ejemplo de ImageView anterior.