Native Module и Native Components — это наши стабильные технологии, используемые в унаследованной архитектуре. Они будут устаревшими в будущем, когда новая архитектура станет стабильной. Новая архитектура использует Turbo Native Module и Fabric Native Components для достижения аналогичных результатов.
Существует масса нативных виджетов пользовательского интерфейса, готовых к использованию в новейших приложениях — некоторые из них являются частью платформы, другие доступны в виде библиотек сторонних разработчиков, а еще больше могут использоваться в вашем собственном портфолио. В React Native уже обернуто несколько наиболее важных компонентов платформы, таких как ScrollView и TextInput, но не все из них, и уж точно не те, которые вы могли написать сами для предыдущего приложения. К счастью, мы можем обернуть эти существующие компоненты для бесшовной интеграции с вашим приложением React Native.
Как и руководство по нативным модулям, это более продвинутое руководство, которое предполагает, что вы немного знакомы с программированием Android SDK. Это руководство покажет вам, как создать нативный компонент пользовательского интерфейса, проведя вас через реализацию подмножества существующего компонента ImageView, доступного в основной библиотеке React Native.
В этом примере мы рассмотрим требования к реализации, позволяющие использовать ImageViews в JavaScript.
Нативные представления создаются и манипулируются путем расширения ViewManager или, чаще всего, SimpleViewManager. В данном случае SimpleViewManager удобен тем, что он применяет общие свойства, такие как цвет фона, непрозрачность и Flexbox-макет.
Эти подклассы по сути являются синглтонами — мост создает только один экземпляр каждого из них. Они отправляют собственные представления в NativeViewHierarchyManager, который делегирует им обратную связь для установки и обновления свойств представлений по мере необходимости. Менеджеры ViewManagers также обычно являются делегатами для представлений, посылая события обратно в JavaScript через мост.
Чтобы отправить представление:
Создайте подкласс ViewManager.
Реализуйте метод createViewInstance.
Раскройте сеттеры свойств представления, используя аннотацию @ReactProp (или @ReactPropGroup).
Зарегистрируйте менеджер в createViewManagers пакета приложений.
В этом примере мы создаем класс менеджера представлений ReactImageManager, который расширяет SimpleViewManager типа ReactImageView. ReactImageView — это тип объекта, управляемого менеджером, это будет пользовательское нативное представление. Имя, возвращаемое функцией getName, используется для ссылки на тип нативного представления из JavaScript.
Представления создаются в методе createViewInstance, представление должно инициализировать себя в состоянии по умолчанию, любые свойства будут установлены через последующий вызов updateView..
3. Выявление сеттеров свойств представления с помощью аннотации @ReactProp (или @ReactPropGroup)¶
Свойства, которые должны быть отражены в JavaScript, должны быть представлены в виде метода setter, аннотированного @ReactProp (или @ReactPropGroup). Метод setter должен принимать обновляемое представление (текущего типа представления) в качестве первого аргумента и значение свойства в качестве второго аргумента. Setter должен быть публичным и не возвращать значение (т.е. тип возврата должен быть void в Java или Unit в Kotlin). Тип свойства, передаваемого в JS, определяется автоматически на основе типа значения аргумента сеттера. В настоящее время поддерживаются следующие типы значений (в Java): boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap. Соответствующие типы в Kotlin: Boolean, Int, Float, Double, String, ReadableArray, ReadableMap.
Аннотация @ReactProp имеет один обязательный аргумент name типа String. Имя, присвоенное аннотации @ReactProp, связанной с методом сеттера, используется для ссылки на свойство на стороне JS.
Кроме name, аннотация @ReactProp может принимать следующие необязательные аргументы: defaultBoolean, defaultInt, defaultFloat. Эти аргументы должны быть соответствующего типа (соответственно boolean, int, float в Java и Boolean, Int, Float в Kotlin), а значение будет передано методу сеттера в случае, если свойство, на которое ссылается сеттер, было удалено из компонента. Обратите внимание, что значения "по умолчанию" предоставляются только для примитивных типов, в случае, если сеттер имеет какой-то сложный тип, null будет предоставлено в качестве значения по умолчанию в случае, если соответствующее свойство будет удалено.
Требования к декларации сеттера для методов, аннотированных с @ReactPropGroup, отличаются от требований для @ReactProp, пожалуйста, обратитесь к документации класса аннотации @ReactPropGroup для получения дополнительной информации об этом. ВАЖНО! В ReactJS обновление значения свойства приведет к вызову метода setter. Обратите внимание, что одним из способов обновления компонента является удаление свойств, которые были установлены ранее. В этом случае также будет вызван метод setter, чтобы уведомить менеджер представления о том, что свойство изменилось. В этом случае будет предоставлено значение "по умолчанию" (для примитивных типов значение "по умолчанию" можно указать с помощью аргументов defaultBoolean, defaultFloat и т.д. аннотации @ReactProp, для сложных типов setter будет вызван со значением null).
Последним шагом является регистрация ViewManager в приложении, это происходит аналогично Native Modules, через функцию-член пакета applications createViewManagers.
Самый последний шаг — создание модуля JavaScript, который определяет интерфейсный слой между Java/Kotlin и JavaScript для пользователей вашего нового представления. Рекомендуется документировать интерфейс компонента в этом модуле (например, используя TypeScript, Flow или обычные комментарии).
Функция requireNativeComponent принимает имя нативного представления. Обратите внимание, что если ваш компонент должен делать что-то более сложное (например, пользовательскую обработку событий), вам следует обернуть родной компонент в другой компонент React. Это показано в примере MyCustomView ниже.
Итак, теперь мы знаем, как открывать нативные компоненты представления, которыми мы можем свободно управлять из JS, но как нам быть с событиями от пользователя, такими как пинч-зум или панорамирование? При возникновении нативного события нативный код должен выдать событие JavaScript-представлению представления, и два представления будут связаны значением, возвращаемым методом getId().
Чтобы сопоставить имя события topChange с пропсом обратного вызова onChange в JavaScript, зарегистрируйте его, переопределив метод getExportedCustomBubblingEventTypeConstants в вашем ViewManager:
Этот обратный вызов вызывается с необработанным событием, которое мы обычно обрабатываем в компоненте-обертке, чтобы упростить API:
MyCustomView.tsx
1 2 3 4 5 6 7 8 9101112131415161718192021222324
classMyCustomViewextendsReact.Component{constructor(props){super(props);this._onChange=this._onChange.bind(this);}_onChange(event){if(!this.props.onChangeMessage){return;}this.props.onChangeMessage(event.nativeEvent.message);}render(){return<RCTMyCustomView{...this.props}onChange={this._onChange}/>;}}MyCustomView.propTypes={/** * Callback that is called continuously when the user is dragging the map. */onChangeMessage:PropTypes.func,...};constRCTMyCustomView=requireNativeComponent(`RCTMyCustomView`);
Для того чтобы интегрировать существующие нативные элементы пользовательского интерфейса в ваше приложение React Native, вам может понадобиться использовать Android Fragments, чтобы дать вам более детальный контроль над вашим нативным компонентом, чем возвращение View из вашего ViewManager. Это понадобится вам, если вы хотите добавить пользовательскую логику, привязанную к вашему представлению с помощью методов жизненного цикла, таких как onViewCreated, onPause, onResume. Следующие шаги покажут вам, как это сделать:
1. Создание примера пользовательского представления¶
Сначала создадим класс CustomView, который расширяет FrameLayout (содержимым этого представления может быть любое представление, которое вы хотите отобразить)
CustomView.kt
1 2 3 4 5 6 7 8 91011121314151617181920
// replace with your packagepackagecom.mypackageimportandroid.content.Contextimportandroid.graphics.Colorimportandroid.widget.FrameLayoutimportandroid.widget.TextViewclassCustomView(context:Context):FrameLayout(context){init{// set padding and background colorsetPadding(16,16,16,16)setBackgroundColor(Color.parseColor("#5FD3F3"))// add default text viewaddView(TextView(context).apply{text="Welcome to Android Fragments with React Native."})}}
CustomView.java
1 2 3 4 5 6 7 8 9101112131415161718192021222324
// replace with your packagepackagecom.mypackage;importandroid.content.Context;importandroid.graphics.Color;importandroid.widget.FrameLayout;importandroid.widget.ImageView;importandroid.widget.TextView;importandroidx.annotation.NonNull;publicclassCustomViewextendsFrameLayout{publicCustomView(@NonNullContextcontext){super(context);// set padding and background colorthis.setPadding(16,16,16,16);this.setBackgroundColor(Color.parseColor("#5FD3F3"));// add default text viewTextViewtext=newTextView(context);text.setText("Welcome to Android Fragments with React Native.");this.addView(text);}}
// replace with your packagepackagecom.mypackageimportandroid.os.Bundleimportandroid.view.LayoutInflaterimportandroid.view.Viewimportandroid.view.ViewGroupimportandroidx.fragment.app.Fragment// replace with your view's importimportcom.mypackage.CustomViewclassMyFragment:Fragment(){privatelateinitvarcustomView:CustomViewoverridefunonCreateView(inflater:LayoutInflater,container:ViewGroup?,savedInstanceState:Bundle?):View{super.onCreateView(inflater,container,savedInstanceState)customView=CustomView(requireNotNull(context))returncustomView// this CustomView could be any view that you want to render}overridefunonViewCreated(view:View,savedInstanceState:Bundle?){super.onViewCreated(view,savedInstanceState)// do any logic that should happen in an `onCreate` method, e.g:// customView.onCreate(savedInstanceState);}overridefunonPause(){super.onPause()// do any logic that should happen in an `onPause` method// e.g.: customView.onPause();}overridefunonResume(){super.onResume()// do any logic that should happen in an `onResume` method// e.g.: customView.onResume();}overridefunonDestroy(){super.onDestroy()// do any logic that should happen in an `onDestroy` method// e.g.: customView.onDestroy();}}
// replace with your packagepackagecom.mypackage;importandroid.os.Bundle;importandroid.view.LayoutInflater;importandroid.view.View;importandroid.view.ViewGroup;importandroidx.fragment.app.Fragment;// replace with your view's importimportcom.mypackage.CustomView;publicclassMyFragmentextendsFragment{CustomViewcustomView;@OverridepublicViewonCreateView(LayoutInflaterinflater,ViewGroupparent,BundlesavedInstanceState){super.onCreateView(inflater,parent,savedInstanceState);customView=newCustomView(this.getContext());returncustomView;// this CustomView could be any view that you want to render}@OverridepublicvoidonViewCreated(Viewview,BundlesavedInstanceState){super.onViewCreated(view,savedInstanceState);// do any logic that should happen in an `onCreate` method, e.g:// customView.onCreate(savedInstanceState);}@OverridepublicvoidonPause(){super.onPause();// do any logic that should happen in an `onPause` method// e.g.: customView.onPause();}@OverridepublicvoidonResume(){super.onResume();// do any logic that should happen in an `onResume` method// e.g.: customView.onResume();}@OverridepublicvoidonDestroy(){super.onDestroy();// do any logic that should happen in an `onDestroy` method// e.g.: customView.onDestroy();}}
// replace with your packagepackagecom.mypackageimportandroid.view.Choreographerimportandroid.view.Viewimportandroid.view.ViewGroupimportandroid.widget.FrameLayoutimportandroidx.fragment.app.FragmentActivityimportcom.facebook.react.bridge.ReactApplicationContextimportcom.facebook.react.bridge.ReadableArrayimportcom.facebook.react.uimanager.ThemedReactContextimportcom.facebook.react.uimanager.ViewGroupManagerimportcom.facebook.react.uimanager.annotations.ReactPropGroupclassMyViewManager(privatevalreactContext:ReactApplicationContext):ViewGroupManager<FrameLayout>(){privatevarpropWidth:Int?=nullprivatevarpropHeight:Int?=nulloverridefungetName()=REACT_CLASS/*** Return a FrameLayout which will later hold the Fragment*/overridefuncreateViewInstance(reactContext:ThemedReactContext)=FrameLayout(reactContext)/*** Map the "create" command to an integer*/overridefungetCommandsMap()=mapOf("create"toCOMMAND_CREATE)/*** Handle "create" command (called from JS) and call createFragment method*/overridefunreceiveCommand(root:FrameLayout,commandId:String,args:ReadableArray?){super.receiveCommand(root,commandId,args)valreactNativeViewId=requireNotNull(args).getInt(0)when(commandId.toInt()){COMMAND_CREATE->createFragment(root,reactNativeViewId)}}@ReactPropGroup(names=["width","height"],customType="Style")funsetStyle(view:FrameLayout,index:Int,value:Int){if(index==0)propWidth=valueif(index==1)propHeight=value}/*** Replace your React Native view with a custom fragment*/funcreateFragment(root:FrameLayout,reactNativeViewId:Int){valparentView=root.findViewById<ViewGroup>(reactNativeViewId)setupLayout(parentView)valmyFragment=MyFragment()valactivity=reactContext.currentActivityasFragmentActivityactivity.supportFragmentManager.beginTransaction().replace(reactNativeViewId,myFragment,reactNativeViewId.toString()).commit()}funsetupLayout(view:View){Choreographer.getInstance().postFrameCallback(object:Choreographer.FrameCallback{overridefundoFrame(frameTimeNanos:Long){manuallyLayoutChildren(view)view.viewTreeObserver.dispatchOnGlobalLayout()Choreographer.getInstance().postFrameCallback(this)}})}/*** Layout all children properly*/privatefunmanuallyLayoutChildren(view:View){// propWidth and propHeight coming from react-native propsvalwidth=requireNotNull(propWidth)valheight=requireNotNull(propHeight)view.measure(View.MeasureSpec.makeMeasureSpec(width,View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(height,View.MeasureSpec.EXACTLY))view.layout(0,0,width,height)}companionobject{privateconstvalREACT_CLASS="MyViewManager"privateconstvalCOMMAND_CREATE=1}}
// replace with your packagepackagecom.mypackage;importandroid.view.Choreographer;importandroid.view.View;importandroid.widget.FrameLayout;importandroidx.annotation.NonNull;importandroidx.annotation.Nullable;importandroidx.fragment.app.FragmentActivity;importcom.facebook.react.bridge.ReactApplicationContext;importcom.facebook.react.bridge.ReadableArray;importcom.facebook.react.common.MapBuilder;importcom.facebook.react.uimanager.annotations.ReactProp;importcom.facebook.react.uimanager.annotations.ReactPropGroup;importcom.facebook.react.uimanager.ViewGroupManager;importcom.facebook.react.uimanager.ThemedReactContext;importjava.util.Map;publicclassMyViewManagerextendsViewGroupManager<FrameLayout>{publicstaticfinalStringREACT_CLASS="MyViewManager";publicfinalintCOMMAND_CREATE=1;privateintpropWidth;privateintpropHeight;ReactApplicationContextreactContext;publicMyViewManager(ReactApplicationContextreactContext){this.reactContext=reactContext;}@OverridepublicStringgetName(){returnREACT_CLASS;}/** * Return a FrameLayout which will later hold the Fragment */@OverridepublicFrameLayoutcreateViewInstance(ThemedReactContextreactContext){returnnewFrameLayout(reactContext);}/** * Map the "create" command to an integer */@Nullable@OverridepublicMap<String,Integer>getCommandsMap(){returnMapBuilder.of("create",COMMAND_CREATE);}/** * Handle "create" command (called from JS) and call createFragment method */@OverridepublicvoidreceiveCommand(@NonNullFrameLayoutroot,StringcommandId,@NullableReadableArrayargs){super.receiveCommand(root,commandId,args);intreactNativeViewId=args.getInt(0);intcommandIdInt=Integer.parseInt(commandId);switch(commandIdInt){caseCOMMAND_CREATE:createFragment(root,reactNativeViewId);break;default:{}}}@ReactPropGroup(names={"width","height"},customType="Style")publicvoidsetStyle(FrameLayoutview,intindex,Integervalue){if(index==0){propWidth=value;}if(index==1){propHeight=value;}}/** * Replace your React Native view with a custom fragment */publicvoidcreateFragment(FrameLayoutroot,intreactNativeViewId){ViewGroupparentView=(ViewGroup)root.findViewById(reactNativeViewId);setupLayout(parentView);finalMyFragmentmyFragment=newMyFragment();FragmentActivityactivity=(FragmentActivity)reactContext.getCurrentActivity();activity.getSupportFragmentManager().beginTransaction().replace(reactNativeViewId,myFragment,String.valueOf(reactNativeViewId)).commit();}publicvoidsetupLayout(Viewview){Choreographer.getInstance().postFrameCallback(newChoreographer.FrameCallback(){@OverridepublicvoiddoFrame(longframeTimeNanos){manuallyLayoutChildren(view);view.getViewTreeObserver().dispatchOnGlobalLayout();Choreographer.getInstance().postFrameCallback(this);}});}/** * Layout all children properly */publicvoidmanuallyLayoutChildren(Viewview){// propWidth and propHeight coming from react-native propsintwidth=propWidth;intheight=propHeight;view.measure(View.MeasureSpec.makeMeasureSpec(width,View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(height,View.MeasureSpec.EXACTLY));view.layout(0,0,width,height);}}
// replace with your packagepackagecom.mypackageimportcom.facebook.react.ReactPackageimportcom.facebook.react.bridge.ReactApplicationContextimportcom.facebook.react.uimanager.ViewManagerclassMyPackage:ReactPackage{...overridefuncreateViewManagers(reactContext:ReactApplicationContext)=listOf(MyViewManager(reactContext))}
MyPackage.java
1 2 3 4 5 6 7 8 91011121314151617181920
// replace with your packagepackagecom.mypackage;importcom.facebook.react.ReactPackage;importcom.facebook.react.bridge.ReactApplicationContext;importcom.facebook.react.uimanager.ViewManager;importjava.util.Arrays;importjava.util.List;publicclassMyPackageimplementsReactPackage{@OverridepublicList<ViewManager>createViewManagers(ReactApplicationContextreactContext){returnArrays.<ViewManager>asList(newMyViewManager(reactContext));}}
importReact,{useEffect,useRef}from'react';import{PixelRatio,UIManager,findNodeHandle,}from'react-native';import{MyViewManager}from'./my-view-manager';constcreateFragment=(viewId)=>UIManager.dispatchViewManagerCommand(viewId,// we are calling the 'create' commandUIManager.MyViewManager.Commands.create.toString(),[viewId]);exportconstMyView=()=>{constref=useRef(null);useEffect(()=>{constviewId=findNodeHandle(ref.current);createFragment(viewId);},[]);return(<MyViewManagerstyle={{// converts dpi to px, provide desired heightheight:PixelRatio.getPixelSizeForLayoutSize(200),// converts dpi to px, provide desired widthwidth:PixelRatio.getPixelSizeForLayoutSize(200),}}ref={ref}/>);};
Если вы хотите раскрыть сеттеры свойств, используя аннотацию @ReactProp (или @ReactPropGroup), смотрите пример ImageView выше.