diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abccd9dd..c9b452e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,62 @@ jobs: run: | yarn turbo run build:android --cache-dir="${{ env.TURBO_CACHE_DIR }}" + build-android-newarch: + runs-on: ubuntu-latest + env: + TURBO_CACHE_DIR: .turbo/android-newarch + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup + uses: ./.github/actions/setup + + - name: Cache turborepo for Android new arch + uses: actions/cache@v3 + with: + path: ${{ env.TURBO_CACHE_DIR }} + key: ${{ runner.os }}-turborepo-android-newarch-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-turborepo-android-newarch- + + - name: Check turborepo cache for Android new arch + run: | + TURBO_CACHE_STATUS=$(node -p "($(yarn turbo run build:android:fabric --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:android:fabric').cache.status") + + if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then + echo "turbo_cache_hit=1" >> $GITHUB_ENV + fi + + - name: Install JDK + if: env.turbo_cache_hit != 1 + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Finalize Android SDK + if: env.turbo_cache_hit != 1 + run: | + /bin/bash -c "yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null" + + - name: Cache Gradle + if: env.turbo_cache_hit != 1 + uses: actions/cache@v3 + with: + path: | + ~/.gradle/wrapper + ~/.gradle/caches + key: ${{ runner.os }}-gradle-newarch-${{ hashFiles('example/android/gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle-newarch- + + - name: Build example for Android new arch + env: + JAVA_OPTS: "-XX:MaxHeapSize=6g" + run: | + yarn turbo run build:android:fabric --cache-dir="${{ env.TURBO_CACHE_DIR }}" + build-ios: runs-on: macos-15 env: diff --git a/android/build.gradle b/android/build.gradle index afa0f093..0fc108f8 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -121,7 +121,7 @@ dependencies { if (isNewArchitectureEnabled()) { react { jsRootDir = file("../src/") - libraryName = "RCTTabView" + libraryName = "RNCTabView" codegenJavaPackageName = "com.rcttabview" } } diff --git a/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt b/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt new file mode 100644 index 00000000..35bc825d --- /dev/null +++ b/android/src/main/java/com/rcttabview/RCTTabViewImpl.kt @@ -0,0 +1,81 @@ +package com.rcttabview + +import android.content.res.ColorStateList +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.common.MapBuilder + +data class TabInfo( + val key: String, + val title: String, + val badge: String, + val activeTintColor: Int? +) + +class RCTTabViewImpl { + fun getName(): String { + return NAME + } + + companion object { + const val NAME = "RNCTabView" + } + + fun setItems(view: ReactBottomNavigationView, items: ReadableArray) { + val itemsArray = mutableListOf() + for (i in 0 until items.size()) { + items.getMap(i).let { item -> + itemsArray.add( + TabInfo( + key = item.getString("key") ?: "", + title = item.getString("title") ?: "", + badge = item.getString("badge") ?: "", + activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null + ) + ) + } + } + view.updateItems(itemsArray) + } + + fun setSelectedPage(view: ReactBottomNavigationView, key: String) { + view.items?.indexOfFirst { it.key == key }?.let { + view.selectedItemId = it + } + } + + fun setLabeled(view: ReactBottomNavigationView, flag: Boolean?) { + view.setLabeled(flag) + } + + fun setIcons(view: ReactBottomNavigationView, icons: ReadableArray?) { + view.setIcons(icons) + } + + fun setBarTintColor(view: ReactBottomNavigationView, color: Int?) { + view.setBarTintColor(color) + } + + fun setRippleColor(view: ReactBottomNavigationView, rippleColor: Int?) { + if (rippleColor != null) { + val color = ColorStateList.valueOf(rippleColor) + view.setRippleColor(color) + } + } + + fun setActiveTintColor(view: ReactBottomNavigationView, color: Int?) { + view.setActiveTintColor(color) + } + + fun setInactiveTintColor(view: ReactBottomNavigationView, color: Int?) { + view.setInactiveTintColor(color) + } + + fun getExportedCustomDirectEventTypeConstants(): MutableMap? { + return MapBuilder.of( + PageSelectedEvent.EVENT_NAME, + MapBuilder.of("registrationName", "onPageSelected"), + TabLongPressEvent.EVENT_NAME, + MapBuilder.of("registrationName", "onTabLongPress") + ) + } +} diff --git a/android/src/main/java/com/rcttabview/RCTTabViewPackage.kt b/android/src/main/java/com/rcttabview/RCTTabViewPackage.kt index f3a2062a..54492e1b 100644 --- a/android/src/main/java/com/rcttabview/RCTTabViewPackage.kt +++ b/android/src/main/java/com/rcttabview/RCTTabViewPackage.kt @@ -9,7 +9,7 @@ import java.util.ArrayList class RCTTabViewPackage : ReactPackage { override fun createViewManagers(reactContext: ReactApplicationContext): List> { val viewManagers: MutableList> = ArrayList() - viewManagers.add(RCTTabViewViewManager()) + viewManagers.add(RCTTabViewManager(reactContext)) return viewManagers } diff --git a/android/src/main/jni/CMakeLists.txt b/android/src/main/jni/CMakeLists.txt new file mode 100644 index 00000000..71147d64 --- /dev/null +++ b/android/src/main/jni/CMakeLists.txt @@ -0,0 +1,87 @@ +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE ON) + +set(LIB_LITERAL RNCTabView) +set(LIB_TARGET_NAME react_codegen_${LIB_LITERAL}) + +set(LIB_ANDROID_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../..) +set(LIB_COMMON_DIR ${LIB_ANDROID_DIR}/../common/cpp) +set(LIB_ANDROID_GENERATED_JNI_DIR ${LIB_ANDROID_DIR}/build/generated/source/codegen/jni) +set(LIB_ANDROID_GENERATED_COMPONENTS_DIR ${LIB_ANDROID_GENERATED_JNI_DIR}/react/renderer/components/${LIB_LITERAL}) + +add_compile_options( + -fexceptions + -frtti + -std=c++20 + -Wall + -Wpedantic + -Wno-gnu-zero-variadic-macro-arguments +) + +file(GLOB LIB_CUSTOM_SRCS CONFIGURE_DEPENDS *.cpp ${LIB_COMMON_DIR}/react/renderer/components/${LIB_LITERAL}/*.cpp) +file(GLOB LIB_CODEGEN_SRCS CONFIGURE_DEPENDS ${LIB_ANDROID_GENERATED_JNI_DIR}/*.cpp ${LIB_ANDROID_GENERATED_COMPONENTS_DIR}/*.cpp) + +add_library( + ${LIB_TARGET_NAME} + SHARED + ${LIB_CUSTOM_SRCS} + ${LIB_CODEGEN_SRCS} +) + +target_include_directories( + ${LIB_TARGET_NAME} + PUBLIC + . + ${LIB_COMMON_DIR} + ${LIB_ANDROID_GENERATED_JNI_DIR} + ${LIB_ANDROID_GENERATED_COMPONENTS_DIR} +) + +# https://github.com/react-native-community/discussions-and-proposals/discussions/816 +# This if-then-else can be removed once this library does not support version below 0.76 +if (REACTNATIVE_MERGED_SO) + target_link_libraries( + ${LIB_TARGET_NAME} + fbjni + jsi + reactnative + ) +else() + target_link_libraries( + ${LIB_TARGET_NAME} + fbjni + folly_runtime + glog + jsi + react_codegen_rncore + react_debug + react_render_componentregistry + react_render_core + react_render_debug + react_render_graphics + react_render_imagemanager + react_render_mapbuffer + react_utils + react_nativemodule_core + rrc_image + turbomodulejsijni + rrc_view + yoga + ) +endif() + +target_compile_options( + ${LIB_TARGET_NAME} + PRIVATE + -DLOG_TAG=\"ReactNative\" + -fexceptions + -frtti + -std=c++20 + -Wall +) + +target_include_directories( + ${CMAKE_PROJECT_NAME} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/android/src/main/jni/RNCTabView.h b/android/src/main/jni/RNCTabView.h new file mode 100644 index 00000000..f7a1fb25 --- /dev/null +++ b/android/src/main/jni/RNCTabView.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { +JSI_EXPORT +std::shared_ptr RNCTabView_ModuleProvider( + const std::string &moduleName, + const JavaTurboModule::InitParams ¶ms); +} diff --git a/android/src/newarch/RCTTabViewManager.kt b/android/src/newarch/RCTTabViewManager.kt new file mode 100644 index 00000000..b71e9c0a --- /dev/null +++ b/android/src/newarch/RCTTabViewManager.kt @@ -0,0 +1,133 @@ +package com.rcttabview + +import android.content.Context +import android.view.View +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.uimanager.PixelUtil.toDIPFromPixel +import com.facebook.react.uimanager.SimpleViewManager +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.ViewManagerDelegate +import com.facebook.react.viewmanagers.RNCTabViewManagerDelegate +import com.facebook.react.viewmanagers.RNCTabViewManagerInterface +import com.facebook.yoga.YogaMeasureMode +import com.facebook.yoga.YogaMeasureOutput + + +@ReactModule(name = RCTTabViewImpl.NAME) +class RCTTabViewManager(context: ReactApplicationContext) : + SimpleViewManager(), + RNCTabViewManagerInterface { + + private val contextInner: ReactApplicationContext = context + private val delegate: RNCTabViewManagerDelegate = + RNCTabViewManagerDelegate(this) + private val tabViewImpl: RCTTabViewImpl = RCTTabViewImpl() + + override fun createViewInstance(context: ThemedReactContext): ReactBottomNavigationView { + val view = ReactBottomNavigationView(context) + val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id) + view.onTabSelectedListener = { data -> + data.getString("key")?.let { + eventDispatcher?.dispatchEvent(PageSelectedEvent(viewTag = view.id, key = it)) + } + } + + view.onTabLongPressedListener = { data -> + data.getString("key")?.let { + eventDispatcher?.dispatchEvent(TabLongPressEvent(viewTag = view.id, key = it)) + } + } + return view + + } + + override fun getName(): String { + return tabViewImpl.getName() + } + + override fun setItems(view: ReactBottomNavigationView?, value: ReadableArray?) { + if (view != null && value != null) + tabViewImpl.setItems(view, value) + } + + override fun setSelectedPage(view: ReactBottomNavigationView?, value: String?) { + if (view != null && value != null) + tabViewImpl.setSelectedPage(view, value) + } + + override fun setIcons(view: ReactBottomNavigationView?, value: ReadableArray?) { + if (view != null) + tabViewImpl.setIcons(view, value) + } + + override fun setLabeled(view: ReactBottomNavigationView?, value: Boolean) { + if (view != null) + tabViewImpl.setLabeled(view, value) + } + + override fun setRippleColor(view: ReactBottomNavigationView?, value: Int?) { + if (view != null && value != null) + tabViewImpl.setRippleColor(view, value) + } + + override fun setBarTintColor(view: ReactBottomNavigationView?, value: Int?) { + if (view != null && value != null) + tabViewImpl.setBarTintColor(view, value) + } + + override fun setActiveTintColor(view: ReactBottomNavigationView?, value: Int?) { + if (view != null && value != null) + tabViewImpl.setActiveTintColor(view, value) + } + + override fun setInactiveTintColor(view: ReactBottomNavigationView?, value: Int?) { + if (view != null && value != null) + tabViewImpl.setInactiveTintColor(view, value) + } + + override fun getDelegate(): ViewManagerDelegate { + return delegate + } + + public override fun measure( + context: Context?, + localData: ReadableMap?, + props: ReadableMap?, + state: ReadableMap?, + width: Float, + widthMode: YogaMeasureMode?, + height: Float, + heightMode: YogaMeasureMode?, + attachmentsPositions: FloatArray? + ): Long { + val view = ReactBottomNavigationView(context ?: contextInner) + val measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + view.measure(measureSpec, measureSpec) + + return YogaMeasureOutput.make( + toDIPFromPixel(view.measuredWidth.toFloat()), + toDIPFromPixel(view.measuredHeight.toFloat()) + ) + } + + // iOS Methods + + override fun setTranslucent(view: ReactBottomNavigationView?, value: Boolean) { + } + + override fun setIgnoresTopSafeArea(view: ReactBottomNavigationView?, value: Boolean) { + } + + override fun setDisablePageAnimations(view: ReactBottomNavigationView?, value: Boolean) { + } + + override fun setSidebarAdaptable(view: ReactBottomNavigationView?, value: Boolean) { + } + + override fun setScrollEdgeAppearance(view: ReactBottomNavigationView?, value: String?) { + } +} diff --git a/android/src/newarch/RCTTabViewViewManager.kt b/android/src/newarch/RCTTabViewViewManager.kt deleted file mode 100644 index ecad49be..00000000 --- a/android/src/newarch/RCTTabViewViewManager.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.rcttabview - -import android.view.View - -import com.facebook.react.bridge.ReactApplicationContext -import com.facebook.react.uimanager.SimpleViewManager -import com.facebook.react.uimanager.ViewManagerDelegate -import com.facebook.react.viewmanagers.SwiftuiTabviewViewManagerDelegate -import com.facebook.react.viewmanagers.SwiftuiTabviewViewManagerInterface - -abstract class RCTTabViewViewManagerSpec : SimpleViewManager(), SwiftuiTabviewViewManagerInterface { - private val mDelegate: ViewManagerDelegate - - init { - mDelegate = SwiftuiTabviewViewManagerDelegate(this) - } - - override fun getDelegate(): ViewManagerDelegate? { - return mDelegate - } -} diff --git a/android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt b/android/src/oldarch/RCTTabViewManager.kt similarity index 70% rename from android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt rename to android/src/oldarch/RCTTabViewManager.kt index 3fb3546f..41b582b1 100644 --- a/android/src/main/java/com/rcttabview/RCTTabViewViewManager.kt +++ b/android/src/oldarch/RCTTabViewManager.kt @@ -1,113 +1,107 @@ package com.rcttabview -import android.content.res.ColorStateList -import android.graphics.Color import android.view.View.MeasureSpec import com.facebook.react.bridge.ReadableArray -import com.facebook.react.common.MapBuilder import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.LayoutShadowNode import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext -import com.facebook.react.uimanager.UIManagerModule import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.uimanager.events.EventDispatcher import com.facebook.yoga.YogaMeasureFunction import com.facebook.yoga.YogaMeasureMode import com.facebook.yoga.YogaMeasureOutput import com.facebook.yoga.YogaNode +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.UIManagerModule - -data class TabInfo( - val key: String, - val title: String, - val badge: String, - val activeTintColor: Int? -) - -@ReactModule(name = RCTTabViewViewManager.NAME) -class RCTTabViewViewManager : - SimpleViewManager() { +@ReactModule(name = RCTTabViewImpl.NAME) +class RCTTabViewManager(context: ReactApplicationContext) : SimpleViewManager() { private lateinit var eventDispatcher: EventDispatcher + private var tabViewImpl = RCTTabViewImpl() override fun getName(): String { - return NAME + return tabViewImpl.getName() } - @ReactProp(name = "items") - fun setItems(view: ReactBottomNavigationView, items: ReadableArray) { - val itemsArray = mutableListOf() - for (i in 0 until items.size()) { - items.getMap(i).let { item -> - itemsArray.add( - TabInfo( - key = item.getString("key") ?: "", - title = item.getString("title") ?: "", - badge = item.getString("badge") ?: "", - activeTintColor = if (item.hasKey("activeTintColor")) item.getInt("activeTintColor") else null - ) - ) + public override fun createViewInstance(context: ThemedReactContext): ReactBottomNavigationView { + eventDispatcher = context.getNativeModule(UIManagerModule::class.java)!!.eventDispatcher + val view = ReactBottomNavigationView(context) + view.onTabSelectedListener = { data -> + data.getString("key")?.let { + eventDispatcher.dispatchEvent(PageSelectedEvent(viewTag = view.id, key = it)) + } + } + + view.onTabLongPressedListener = { data -> + data.getString("key")?.let { + eventDispatcher.dispatchEvent(TabLongPressEvent(viewTag = view.id, key = it)) } } - view.updateItems(itemsArray) + return view + } + + override fun createShadowNodeInstance(): LayoutShadowNode { + return TabViewShadowNode() + } + + override fun getExportedCustomDirectEventTypeConstants(): MutableMap? { + return tabViewImpl.getExportedCustomDirectEventTypeConstants() + } + + @ReactProp(name = "items") + fun setItems(view: ReactBottomNavigationView, items: ReadableArray) { + tabViewImpl.setItems(view, items) } @ReactProp(name = "selectedPage") fun setSelectedPage(view: ReactBottomNavigationView, key: String) { - view.items?.indexOfFirst { it.key == key }?.let { - view.selectedItemId = it - } + tabViewImpl.setSelectedPage(view, key) } + @ReactProp(name = "labeled") fun setLabeled(view: ReactBottomNavigationView, flag: Boolean?) { - view.setLabeled(flag) + tabViewImpl.setLabeled(view, flag) } @ReactProp(name = "icons") fun setIcons(view: ReactBottomNavigationView, icons: ReadableArray?) { - view.setIcons(icons) + tabViewImpl.setIcons(view, icons) } @ReactProp(name = "barTintColor", customType = "Color") fun setBarTintColor(view: ReactBottomNavigationView, color: Int?) { - view.setBarTintColor(color) + tabViewImpl.setBarTintColor(view, color) } @ReactProp(name = "rippleColor", customType = "Color") fun setRippleColor(view: ReactBottomNavigationView, rippleColor: Int?) { - if (rippleColor != null) { - val color = ColorStateList.valueOf(rippleColor) - view.setRippleColor(color) - } + tabViewImpl.setRippleColor(view, rippleColor) } @ReactProp(name = "activeTintColor", customType = "Color") fun setActiveTintColor(view: ReactBottomNavigationView, color: Int?) { - view.setActiveTintColor(color) + tabViewImpl.setActiveTintColor(view, color) } @ReactProp(name = "inactiveTintColor", customType = "Color") fun setInactiveTintColor(view: ReactBottomNavigationView, color: Int?) { - view.setInactiveTintColor(color) + tabViewImpl.setInactiveTintColor(view, color) } - public override fun createViewInstance(context: ThemedReactContext): ReactBottomNavigationView { - eventDispatcher = context.getNativeModule(UIManagerModule::class.java)!!.eventDispatcher - val view = ReactBottomNavigationView(context) - view.onTabSelectedListener = { data -> - data.getString("key")?.let { - eventDispatcher.dispatchEvent(PageSelectedEvent(viewTag = view.id, key = it)) - } - } + // iOS Props - view.onTabLongPressedListener = { data -> - data.getString("key")?.let { - eventDispatcher.dispatchEvent(TabLongPressEvent(viewTag = view.id, key = it)) - } - } + @ReactProp(name = "sidebarAdaptable") + fun setSidebarAdaptable(view: ReactBottomNavigationView, flag: Boolean) { + } - return view + @ReactProp(name = "ignoresTopSafeArea") + fun setIgnoresTopSafeArea(view: ReactBottomNavigationView, flag: Boolean) { + } + + @ReactProp(name = "disablePageAnimations") + fun setDisablePageAnimations(view: ReactBottomNavigationView, flag: Boolean) { } class TabViewShadowNode() : LayoutShadowNode(), @@ -145,39 +139,4 @@ class RCTTabViewViewManager : return YogaMeasureOutput.make(mWidth, mHeight) } } - - override fun createShadowNodeInstance(): LayoutShadowNode { - return TabViewShadowNode() - } - - companion object { - const val NAME = "RNCTabView" - } - - override fun getExportedCustomDirectEventTypeConstants(): MutableMap? { - return MapBuilder.of( - PageSelectedEvent.EVENT_NAME, - MapBuilder.of("registrationName", "onPageSelected"), - TabLongPressEvent.EVENT_NAME, - MapBuilder.of("registrationName", "onTabLongPress") - ) - } - - // iOS Props - - @ReactProp(name = "sidebarAdaptable") - fun setSidebarAdaptable(view: ReactBottomNavigationView, flag: Boolean) { - } - - @ReactProp(name = "ignoresTopSafeArea") - fun setIgnoresTopSafeArea(view: ReactBottomNavigationView, flag: Boolean) { - } - - @ReactProp(name = "disablePageAnimations") - fun setDisablePageAnimations(view: ReactBottomNavigationView, flag: Boolean) { - } - - @ReactProp(name = "translucent") - fun setTranslucentview(view: ReactBottomNavigationView, translucent: Boolean?) { - } } diff --git a/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h new file mode 100644 index 00000000..9e80b06b --- /dev/null +++ b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewComponentDescriptor.h @@ -0,0 +1,47 @@ +#ifdef __cplusplus + +#pragma once + +#include +#include + +namespace facebook::react { + +class RNCTabViewComponentDescriptor final : public ConcreteComponentDescriptor +{ +#ifdef ANDROID +public: + RNCTabViewComponentDescriptor(const ComponentDescriptorParameters ¶meters) + : ConcreteComponentDescriptor(parameters), measurementsManager_( + std::make_shared(contextContainer_)) {} + + void adopt(ShadowNode &shadowNode) const override + { + ConcreteComponentDescriptor::adopt(shadowNode); + + auto &rncTabViewShadowNode = + static_cast(shadowNode); + + // `RNCTabViewShadowNode` uses `RNCTabViewMeasurementsManager` to + // provide measurements to Yoga. + rncTabViewShadowNode.setSliderMeasurementsManager( + measurementsManager_); + + // All `RNCTabViewShadowNode`s must have leaf Yoga nodes with properly + // setup measure function. + rncTabViewShadowNode.enableMeasurement(); + } + +private: + const std::shared_ptr measurementsManager_; +#else +public: + RNCTabViewComponentDescriptor(const ComponentDescriptorParameters ¶meters) + : ConcreteComponentDescriptor(parameters) {} +#endif + +}; + +} + +#endif diff --git a/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.cpp b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.cpp new file mode 100644 index 00000000..996da9f4 --- /dev/null +++ b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.cpp @@ -0,0 +1,65 @@ +#ifdef ANDROID +#include "RNCTabViewMeasurementsManager.h" + +#include +#include +#include + + +using namespace facebook::jni; + +namespace facebook::react +{ +Size RNCTabViewMeasurementsManager::measure( + SurfaceId surfaceId, + LayoutConstraints layoutConstraints) const +{ + { + std::scoped_lock lock(mutex_); + if (hasBeenMeasured_) + { + return cachedMeasurement_; + } + } + + const jni::global_ref& fabricUIManager = + contextContainer_->at>("FabricUIManager"); + + static auto measure = facebook::jni::findClassStatic( + "com/facebook/react/fabric/FabricUIManager") + ->getMethod("measure"); + + auto minimumSize = layoutConstraints.minimumSize; + auto maximumSize = layoutConstraints.maximumSize; + + local_ref componentName = make_jstring("RNCTabView"); + + auto measurement = yogaMeassureToSize(measure( + fabricUIManager, + surfaceId, + componentName.get(), + nullptr, + nullptr, + nullptr, + minimumSize.width, + maximumSize.width, + minimumSize.height, + maximumSize.height)); + + std::scoped_lock lock(mutex_); + cachedMeasurement_ = measurement; + hasBeenMeasured_ = true; + return measurement; +} +} + +#endif diff --git a/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.h b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.h new file mode 100644 index 00000000..1882ac43 --- /dev/null +++ b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewMeasurementsManager.h @@ -0,0 +1,31 @@ +#ifdef __cplusplus + +#ifdef ANDROID +#pragma once +#include +#include +#include + +namespace facebook::react +{ + + class RNCTabViewMeasurementsManager + { + public: + RNCTabViewMeasurementsManager( + const ContextContainer::Shared &contextContainer) + : contextContainer_(contextContainer) {} + + Size measure(SurfaceId surfaceId, LayoutConstraints layoutConstraints) const; + + private: + const ContextContainer::Shared contextContainer_; + mutable std::mutex mutex_; + mutable bool hasBeenMeasured_ = false; + mutable Size cachedMeasurement_{}; + }; +} + +#endif + +#endif diff --git a/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp new file mode 100644 index 00000000..47867419 --- /dev/null +++ b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.cpp @@ -0,0 +1,28 @@ +#include "RNCTabViewShadowNode.h" +#include "RNCTabViewMeasurementsManager.h" + +namespace facebook::react { + +extern const char RNCTabViewComponentName[] = "RNCTabView"; + +#ifdef ANDROID +void RNCTabViewShadowNode::setSliderMeasurementsManager( + const std::shared_ptr & + measurementsManager) +{ + ensureUnsealed(); + measurementsManager_ = measurementsManager; +} + +#pragma mark - LayoutableShadowNode + +Size RNCTabViewShadowNode::measureContent( + const LayoutContext & /*layoutContext*/, + const LayoutConstraints &layoutConstraints) const +{ + return measurementsManager_->measure(getSurfaceId(), layoutConstraints); +} + +#endif + +} diff --git a/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h new file mode 100644 index 00000000..c195794f --- /dev/null +++ b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewShadowNode.h @@ -0,0 +1,49 @@ +#ifdef __cplusplus + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "RNCTabViewMeasurementsManager.h" + +namespace facebook::react { + +JSI_EXPORT extern const char RNCTabViewComponentName[]; + +/* +* `ShadowNode` for component. +*/ +class JSI_EXPORT RNCTabViewShadowNode final +: public ConcreteViewShadowNode< +RNCTabViewComponentName, +RNCTabViewProps, +RNCTabViewEventEmitter, +RNCTabViewState> +{ +public: + using ConcreteViewShadowNode::ConcreteViewShadowNode; + +#ifdef ANDROID + void setSliderMeasurementsManager( + const std::shared_ptr &measurementsManager); + + #pragma mark - LayoutableShadowNode + + Size measureContent( + const LayoutContext &layoutContext, + const LayoutConstraints &layoutConstraints) const override; + +private: + std::shared_ptr measurementsManager_; +#endif + +}; + +} + +#endif diff --git a/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h new file mode 100644 index 00000000..cabea07e --- /dev/null +++ b/common/cpp/react/renderer/components/RNCTabView/RNCTabViewState.h @@ -0,0 +1,35 @@ +#ifdef __cplusplus + +#pragma once + +#ifdef ANDROID +#include +#include +#include +#endif + +namespace facebook::react { + +class RNCTabViewState +{ +public: + RNCTabViewState() = default; + +#ifdef ANDROID + RNCTabViewState(RNCTabViewState const &previousState, folly::dynamic data) {}; + + folly::dynamic getDynamic() const + { + return {}; + }; + + MapBuffer getMapBuffer() const + { + return MapBufferBuilder::EMPTY(); + }; +#endif +}; + +} + +#endif diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 6b9e8dcd..66ddb865 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -40,7 +40,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 # to write custom TurboModules/Fabric components OR use libraries that # are providing them. # Note that this is incompatible with web debugging. -#newArchEnabled=true +newArchEnabled=false #bridgelessEnabled=true # Uncomment the line below to build React Native from source. diff --git a/example/package.json b/example/package.json index 6e05fd86..9d3ad77a 100644 --- a/example/package.json +++ b/example/package.json @@ -5,6 +5,7 @@ "scripts": { "android": "react-native run-android", "build:android": "npm run mkdist && react-native bundle --entry-file index.js --platform android --dev true --bundle-output dist/main.android.jsbundle --assets-dest dist && react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"", + "build:android:fabric": "npm run mkdist && react-native bundle --entry-file index.js --platform android --dev true --bundle-output dist/main.android.jsbundle --assets-dest dist && react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a -PnewArchEnabled=true\"", "build:ios": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev true --bundle-output dist/main.ios.jsbundle --assets-dest dist && react-native build-ios --scheme ReactNativeBottomTabs --mode Debug --extra-params \"-sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO\"", "build:visionos": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev true --bundle-output dist/main.visionos.jsbundle --assets-dest dist", "ios": "react-native run-ios", diff --git a/example/src/Screens/Albums.tsx b/example/src/Screens/Albums.tsx index 4cb7a381..cf87f64d 100644 --- a/example/src/Screens/Albums.tsx +++ b/example/src/Screens/Albums.tsx @@ -87,7 +87,6 @@ const styles = StyleSheet.create({ }), photo: { flex: 1, - paddingTop: '100%', height: 'auto', width: 'auto', }, diff --git a/ios/Fabric/RCTTabViewComponentView.mm b/ios/Fabric/RCTTabViewComponentView.mm index 104b0d03..c24dbfb3 100644 --- a/ios/Fabric/RCTTabViewComponentView.mm +++ b/ios/Fabric/RCTTabViewComponentView.mm @@ -1,10 +1,11 @@ #ifdef RCT_NEW_ARCH_ENABLED #import "RCTTabViewComponentView.h" -#import -#import -#import -#import +#import +#import +#import +#import +#import #import @@ -48,7 +49,7 @@ - (instancetype)initWithFrame:(CGRect)frame self.contentView = _tabViewProvider; _props = defaultProps; } - + return self; } @@ -75,7 +76,7 @@ - (void)mountChildComponentView:(UIView *)childCompone - (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { [_reactSubviews removeObjectAtIndex:index]; - + [childComponentView removeFromSuperview]; } @@ -83,65 +84,65 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & { const auto &oldViewProps = *std::static_pointer_cast(_props); const auto &newViewProps = *std::static_pointer_cast(props); - + if (haveTabItemsChanged(oldViewProps.items, newViewProps.items)) { _tabViewProvider.itemsData = convertItemsToArray(newViewProps.items); } - + if (oldViewProps.translucent != newViewProps.translucent) { _tabViewProvider.translucent = newViewProps.translucent; } - + if (oldViewProps.icons != newViewProps.icons) { auto iconsArray = [[NSMutableArray alloc] init]; for (auto &source: newViewProps.icons) { auto imageSource = [[RCTImageSource alloc] initWithURLRequest:NSURLRequestFromImageSource(source) size:CGSizeMake(source.size.width, source.size.height) scale:source.scale]; [iconsArray addObject:imageSource]; } - + _tabViewProvider.icons = iconsArray; } - + if (oldViewProps.sidebarAdaptable != newViewProps.sidebarAdaptable) { _tabViewProvider.sidebarAdaptable = newViewProps.sidebarAdaptable; } - + if (oldViewProps.disablePageAnimations != newViewProps.disablePageAnimations) { _tabViewProvider.disablePageAnimations = newViewProps.disablePageAnimations; } - + if (oldViewProps.labeled != newViewProps.labeled) { _tabViewProvider.labeled = newViewProps.labeled; } - + if (oldViewProps.ignoresTopSafeArea != newViewProps.ignoresTopSafeArea) { _tabViewProvider.ignoresTopSafeArea = newViewProps.ignoresTopSafeArea; } - + if (oldViewProps.selectedPage != newViewProps.selectedPage) { _tabViewProvider.selectedPage = RCTNSStringFromString(newViewProps.selectedPage); } - + if (oldViewProps.scrollEdgeAppearance != newViewProps.scrollEdgeAppearance) { _tabViewProvider.scrollEdgeAppearance = RCTNSStringFromString(newViewProps.scrollEdgeAppearance); } - + if (oldViewProps.labeled != newViewProps.labeled) { _tabViewProvider.labeled = newViewProps.labeled; } - + if (oldViewProps.barTintColor != newViewProps.barTintColor) { _tabViewProvider.barTintColor = RCTUIColorFromSharedColor(newViewProps.barTintColor); } - + if (oldViewProps.activeTintColor != newViewProps.activeTintColor) { _tabViewProvider.activeTintColor = RCTUIColorFromSharedColor(newViewProps.activeTintColor); } - + if (oldViewProps.inactiveTintColor != newViewProps.inactiveTintColor) { _tabViewProvider.inactiveTintColor = RCTUIColorFromSharedColor(newViewProps.inactiveTintColor); } - + [super updateProps:props oldProps:oldProps]; } @@ -155,29 +156,29 @@ bool areTabItemsEqual(const RNCTabViewItemsStruct& lhs, const RNCTabViewItemsStr bool haveTabItemsChanged(const std::vector& oldItems, const std::vector& newItems) { - + if (oldItems.size() != newItems.size()) { return true; } - + for (size_t i = 0; i < oldItems.size(); ++i) { if (!areTabItemsEqual(oldItems[i], newItems[i])) { return true; } } - + return false; } NSArray* convertItemsToArray(const std::vector& items) { NSMutableArray *result = [NSMutableArray array]; - + for (const auto& item : items) { auto tabInfo = [[TabInfo alloc] initWithKey:RCTNSStringFromString(item.key) title:RCTNSStringFromString(item.title) badge:RCTNSStringFromString(item.badge) sfSymbol:RCTNSStringFromString(item.sfSymbol) activeTintColor:RCTUIColorFromSharedColor(item.activeTintColor)]; - + [result addObject:tabInfo]; } - + return result; } diff --git a/package.json b/package.json index 7ebb70c1..ead3ab7d 100644 --- a/package.json +++ b/package.json @@ -205,8 +205,11 @@ "use-latest-callback": "^0.2.1" }, "codegenConfig": { - "name": "RNCTabViewSpec", + "name": "RNCTabView", "type": "components", - "jsSrcsDir": "src" + "jsSrcsDir": "src", + "android": { + "javaPackageName": "com.rcttabview" + } } } diff --git a/react-native-bottom-tabs.podspec b/react-native-bottom-tabs.podspec index 13371f70..9b6d531e 100644 --- a/react-native-bottom-tabs.podspec +++ b/react-native-bottom-tabs.podspec @@ -2,6 +2,7 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' +new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1' Pod::Spec.new do |s| s.name = "react-native-bottom-tabs" @@ -16,6 +17,13 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,cpp,swift}" + if new_arch_enabled + s.subspec "common" do |ss| + ss.source_files = "common/cpp/**/*.{cpp,h}" + ss.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/common/cpp\"" } + end + end + s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" } @@ -30,7 +38,7 @@ Pod::Spec.new do |s| s.dependency "React-Core" # Don't install the dependencies when we run `pod install` in the old architecture. - if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then + if new_arch_enabled then s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" s.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", diff --git a/react-native.config.js b/react-native.config.js index 66211dab..b3ebd7d7 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -1,11 +1,10 @@ -/** - * @type {import('@react-native-community/cli-types').UserDependencyConfig} - */ module.exports = { dependency: { platforms: { android: { - cmakeListsPath: 'build/generated/source/codegen/jni/CMakeLists.txt', + libraryName: 'RNCTabView', + componentDescriptors: ['RNCTabViewComponentDescriptor'], + cmakeListsPath: 'src/main/jni/CMakeLists.txt', }, }, }, diff --git a/src/TabViewNativeComponent.ts b/src/TabViewNativeComponent.ts index ead05779..ce7bf364 100644 --- a/src/TabViewNativeComponent.ts +++ b/src/TabViewNativeComponent.ts @@ -34,4 +34,6 @@ export interface TabViewProps extends ViewProps { disablePageAnimations?: boolean; } -export default codegenNativeComponent('RNCTabView'); +export default codegenNativeComponent('RNCTabView', { + interfaceOnly: true, +}); diff --git a/turbo.json b/turbo.json index 405897ee..196b8aaa 100644 --- a/turbo.json +++ b/turbo.json @@ -17,6 +17,22 @@ ], "outputs": [] }, + "build:android:fabric": { + "env": ["ORG_GRADLE_PROJECT_newArchEnabled"], + "inputs": [ + "package.json", + "android", + "!android/build", + "src/*.ts", + "src/*.tsx", + "example/package.json", + "example/android", + "!example/android/.gradle", + "!example/android/build", + "!example/android/app/build" + ], + "outputs": [] + }, "build:ios": { "env": ["RCT_NEW_ARCH_ENABLED"], "inputs": [