first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
buildscript {
def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['RNSAC_kotlinVersion']
repositories {
mavenCentral()
google()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
classpath("com.android.tools.build:gradle:7.3.1")
classpath("com.diffplug.spotless:spotless-plugin-gradle:6.11.0")
}
}
if (project == rootProject) {
apply from: "spotless.gradle"
return
}
def getExtOrDefault(name, defaultValue) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : defaultValue
}
def isNewArchitectureEnabled() {
// To opt-in for the New Architecture, you can either:
// - Set `newArchEnabled` to true inside the `gradle.properties` file
// - Invoke gradle with `-newArchEnabled=true`
// - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
if (isNewArchitectureEnabled()) {
apply plugin: "com.facebook.react"
}
android {
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
if (agpVersion.tokenize('.')[0].toInteger() >= 7) {
namespace "com.th3rdwave.safeareacontext"
buildFeatures {
buildConfig true
}
}
compileSdkVersion getExtOrDefault('compileSdkVersion', 30)
// Used to override the NDK path/version on internal CI or by allowing
// users to customize the NDK path/version from their root project (e.g. for M1 support)
if (rootProject.hasProperty("ndkPath")) {
ndkPath rootProject.ext.ndkPath
}
if (rootProject.hasProperty("ndkVersion")) {
ndkVersion rootProject.ext.ndkVersion
}
defaultConfig {
minSdkVersion getExtOrDefault('minSdkVersion', 16)
targetSdkVersion getExtOrDefault('targetSdkVersion', 28)
versionCode 1
versionName "1.0"
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
ndk {
abiFilters (*reactNativeArchitectures())
}
}
lintOptions{
abortOnError false
}
packagingOptions {
// For some reason gradle only complains about the duplicated version of libreact_render libraries
// while there are more libraries copied in intermediates folder of the lib build directory, we exclude
// only the ones that make the build fail (ideally we should only include libsafeareacontext_modules but we
// are only allowed to specify exclude patterns)
exclude "**/libreact_render*.so"
}
sourceSets.main {
java {
if (isNewArchitectureEnabled()) {
srcDirs += [
"src/fabric/java",
"${project.buildDir}/generated/source/codegen/java"
]
} else {
srcDirs += [
"src/paper/java"
]
}
}
}
}
def reactNativeArchitectures() {
def value = project.getProperties().get("reactNativeArchitectures")
return value ? value.split(",") : [
"armeabi-v7a",
"x86",
"x86_64",
"arm64-v8a"
]
}
repositories {
google()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
mavenCentral()
}
def kotlin_version = getExtOrDefault('kotlinVersion', project.properties['RNSAC_kotlinVersion'])
dependencies {
implementation 'com.facebook.react:react-native:+'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

View File

@@ -0,0 +1 @@
RNSAC_kotlinVersion=1.6.20

View File

@@ -0,0 +1,24 @@
apply plugin: 'com.diffplug.spotless'
allprojects {
repositories {
google()
mavenCentral()
}
}
spotless {
java {
target 'src/**/*.java'
googleJavaFormat()
}
kotlin {
target 'src/**/*.kt'
ktfmt()
}
groovyGradle {
target '*.gradle'
greclipse()
indentWithSpaces(4)
}
}

View File

@@ -0,0 +1,25 @@
package com.th3rdwave.safeareacontext
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
internal class InsetsChangeEvent(
surfaceId: Int,
viewTag: Int,
private val mInsets: EdgeInsets,
private val mFrame: Rect
) : Event<InsetsChangeEvent>(surfaceId, viewTag) {
override fun getEventName() = EVENT_NAME
override fun getEventData(): WritableMap? {
val event = Arguments.createMap()
event.putMap("insets", edgeInsetsToJsMap(mInsets))
event.putMap("frame", rectToJsMap(mFrame))
return event
}
companion object {
const val EVENT_NAME = "topInsetsChange"
}
}

View File

@@ -0,0 +1,14 @@
package com.th3rdwave.safeareacontext
import android.content.Context
import android.view.View
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerHelper
fun getReactContext(view: View): ReactContext {
return UIManagerHelper.getReactContext(view)
}
fun getSurfaceId(context: Context): Int {
return UIManagerHelper.getSurfaceId(context)
}

View File

@@ -0,0 +1,5 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.th3rdwave.safeareacontext">
</manifest>

View File

@@ -0,0 +1,3 @@
package com.th3rdwave.safeareacontext
data class EdgeInsets(val top: Float, val right: Float, val bottom: Float, val left: Float)

View File

@@ -0,0 +1,3 @@
package com.th3rdwave.safeareacontext
data class Rect(val x: Float, val y: Float, val width: Float, val height: Float)

View File

@@ -0,0 +1,32 @@
package com.th3rdwave.safeareacontext
import android.view.View
import android.view.ViewGroup
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule
@ReactModule(name = SafeAreaContextModule.NAME)
class SafeAreaContextModule(reactContext: ReactApplicationContext?) :
NativeSafeAreaContextSpec(reactContext) {
override fun getName(): String {
return NAME
}
public override fun getTypedExportedConstants(): Map<String, Any?> {
return mapOf("initialWindowMetrics" to getInitialWindowMetrics())
}
private fun getInitialWindowMetrics(): Map<String, Any>? {
val decorView = reactApplicationContext.currentActivity?.window?.decorView as ViewGroup?
val contentView = decorView?.findViewById<View>(android.R.id.content) ?: return null
val insets = getSafeAreaInsets(decorView)
val frame = getFrame(decorView, contentView)
return if (insets == null || frame == null) {
null
} else mapOf("insets" to edgeInsetsToJavaMap(insets), "frame" to rectToJavaMap(frame))
}
companion object {
const val NAME = "RNCSafeAreaContext"
}
}

View File

@@ -0,0 +1,41 @@
package com.th3rdwave.safeareacontext
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
// Fool autolinking for older versions that do not support BaseReactPackage.
// public class SafeAreaContextPackage implements ReactPackage {
class SafeAreaContextPackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return when (name) {
SafeAreaContextModule.NAME -> SafeAreaContextModule(reactContext)
else -> null
}
}
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
val moduleList: Array<Class<out NativeModule?>> = arrayOf(SafeAreaContextModule::class.java)
val reactModuleInfoMap: MutableMap<String, ReactModuleInfo> = HashMap()
for (moduleClass in moduleList) {
val reactModule = moduleClass.getAnnotation(ReactModule::class.java) ?: continue
reactModuleInfoMap[reactModule.name] =
ReactModuleInfo(
reactModule.name,
moduleClass.name,
true,
reactModule.needsEagerInit,
reactModule.isCxxModule,
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED)
}
return ReactModuleInfoProvider { reactModuleInfoMap }
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return listOf<ViewManager<*, *>>(SafeAreaProviderManager(), SafeAreaViewManager())
}
}

View File

@@ -0,0 +1,47 @@
package com.th3rdwave.safeareacontext
import android.content.Context
import android.view.ViewGroup
import android.view.ViewTreeObserver
import com.facebook.react.views.view.ReactViewGroup
typealias OnInsetsChangeHandler = (view: SafeAreaProvider, insets: EdgeInsets, frame: Rect) -> Unit
class SafeAreaProvider(context: Context?) :
ReactViewGroup(context), ViewTreeObserver.OnPreDrawListener {
private var mInsetsChangeHandler: OnInsetsChangeHandler? = null
private var mLastInsets: EdgeInsets? = null
private var mLastFrame: Rect? = null
private fun maybeUpdateInsets() {
val insetsChangeHandler = mInsetsChangeHandler ?: return
val edgeInsets = getSafeAreaInsets(this) ?: return
val frame = getFrame(rootView as ViewGroup, this) ?: return
if (mLastInsets != edgeInsets || mLastFrame != frame) {
insetsChangeHandler(this, edgeInsets, frame)
mLastInsets = edgeInsets
mLastFrame = frame
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
viewTreeObserver.addOnPreDrawListener(this)
maybeUpdateInsets()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
viewTreeObserver.removeOnPreDrawListener(this)
}
override fun onPreDraw(): Boolean {
maybeUpdateInsets()
return true
}
fun setOnInsetsChangeHandler(handler: OnInsetsChangeHandler?) {
mInsetsChangeHandler = handler
maybeUpdateInsets()
}
}

View File

@@ -0,0 +1,41 @@
package com.th3rdwave.safeareacontext
import com.facebook.react.bridge.ReactContext
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.ViewGroupManager
import com.facebook.react.viewmanagers.RNCSafeAreaProviderManagerDelegate
import com.facebook.react.viewmanagers.RNCSafeAreaProviderManagerInterface
@ReactModule(name = SafeAreaProviderManager.REACT_CLASS)
class SafeAreaProviderManager :
ViewGroupManager<SafeAreaProvider>(), RNCSafeAreaProviderManagerInterface<SafeAreaProvider> {
private val mDelegate = RNCSafeAreaProviderManagerDelegate(this)
override fun getDelegate() = mDelegate
override fun getName() = REACT_CLASS
public override fun createViewInstance(context: ThemedReactContext) = SafeAreaProvider(context)
override fun getExportedCustomDirectEventTypeConstants() =
mutableMapOf(
InsetsChangeEvent.EVENT_NAME to mutableMapOf("registrationName" to "onInsetsChange"))
override fun addEventEmitters(reactContext: ThemedReactContext, view: SafeAreaProvider) {
super.addEventEmitters(reactContext, view)
view.setOnInsetsChangeHandler(::handleOnInsetsChange)
}
companion object {
const val REACT_CLASS = "RNCSafeAreaProvider"
}
}
private fun handleOnInsetsChange(view: SafeAreaProvider, insets: EdgeInsets, frame: Rect) {
val reactContext = view.context as ReactContext
val reactTag = view.id
UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag)
?.dispatchEvent(InsetsChangeEvent(getSurfaceId(reactContext), reactTag, insets, frame))
}

View File

@@ -0,0 +1,102 @@
package com.th3rdwave.safeareacontext
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import androidx.annotation.RequiresApi
import java.lang.IllegalArgumentException
import kotlin.math.max
import kotlin.math.min
@RequiresApi(Build.VERSION_CODES.R)
private fun getRootWindowInsetsCompatR(rootView: View): EdgeInsets? {
val insets =
rootView.rootWindowInsets?.getInsets(
WindowInsets.Type.statusBars() or
WindowInsets.Type.displayCutout() or
WindowInsets.Type.navigationBars() or
WindowInsets.Type.captionBar())
?: return null
return EdgeInsets(
top = insets.top.toFloat(),
right = insets.right.toFloat(),
bottom = insets.bottom.toFloat(),
left = insets.left.toFloat())
}
@RequiresApi(Build.VERSION_CODES.M)
@Suppress("DEPRECATION")
private fun getRootWindowInsetsCompatM(rootView: View): EdgeInsets? {
val insets = rootView.rootWindowInsets ?: return null
return EdgeInsets(
top = insets.systemWindowInsetTop.toFloat(),
right = insets.systemWindowInsetRight.toFloat(),
// System insets are more reliable to account for notches but the
// system inset for bottom includes the soft keyboard which we don't
// want to be consistent with iOS. Using the min value makes sure we
// never get the keyboard offset while still working with devices that
// hide the navigation bar.
bottom = min(insets.systemWindowInsetBottom, insets.stableInsetBottom).toFloat(),
left = insets.systemWindowInsetLeft.toFloat())
}
private fun getRootWindowInsetsCompatBase(rootView: View): EdgeInsets? {
val visibleRect = android.graphics.Rect()
rootView.getWindowVisibleDisplayFrame(visibleRect)
return EdgeInsets(
top = visibleRect.top.toFloat(),
right = (rootView.width - visibleRect.right).toFloat(),
bottom = (rootView.height - visibleRect.bottom).toFloat(),
left = visibleRect.left.toFloat())
}
private fun getRootWindowInsetsCompat(rootView: View): EdgeInsets? {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> getRootWindowInsetsCompatR(rootView)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> getRootWindowInsetsCompatM(rootView)
else -> getRootWindowInsetsCompatBase(rootView)
}
}
fun getSafeAreaInsets(view: View): EdgeInsets? {
// The view has not been layout yet.
if (view.height == 0) {
return null
}
val rootView = view.rootView
val windowInsets = getRootWindowInsetsCompat(rootView) ?: return null
// Calculate the part of the view that overlaps with window insets.
val windowWidth = rootView.width.toFloat()
val windowHeight = rootView.height.toFloat()
val visibleRect = android.graphics.Rect()
view.getGlobalVisibleRect(visibleRect)
return EdgeInsets(
top = max(windowInsets.top - visibleRect.top, 0f),
right = max(min(visibleRect.left + view.width - windowWidth, 0f) + windowInsets.right, 0f),
bottom = max(min(visibleRect.top + view.height - windowHeight, 0f) + windowInsets.bottom, 0f),
left = max(windowInsets.left - visibleRect.left, 0f))
}
fun getFrame(rootView: ViewGroup, view: View): Rect? {
// This can happen while the view gets unmounted.
if (view.parent == null) {
return null
}
val offset = android.graphics.Rect()
view.getDrawingRect(offset)
try {
rootView.offsetDescendantRectToMyCoords(view, offset)
} catch (ex: IllegalArgumentException) {
// This can throw if the view is not a descendant of rootView. This should not
// happen but avoid potential crashes.
ex.printStackTrace()
return null
}
return Rect(
x = offset.left.toFloat(),
y = offset.top.toFloat(),
width = view.width.toFloat(),
height = view.height.toFloat())
}

View File

@@ -0,0 +1,155 @@
package com.th3rdwave.safeareacontext
import android.content.Context
import android.util.Log
import android.view.View
import android.view.ViewTreeObserver
import com.facebook.react.bridge.Arguments
import com.facebook.react.uimanager.StateWrapper
import com.facebook.react.uimanager.UIManagerModule
import com.facebook.react.views.view.ReactViewGroup
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
private const val MAX_WAIT_TIME_NANO = 500000000L // 500ms
class SafeAreaView(context: Context?) :
ReactViewGroup(context), ViewTreeObserver.OnPreDrawListener {
private var mMode = SafeAreaViewMode.PADDING
private var mInsets: EdgeInsets? = null
private var mEdges: SafeAreaViewEdges? = null
private var mProviderView: View? = null
private var mStateWrapper: StateWrapper? = null
fun getStateWrapper(): StateWrapper? {
return mStateWrapper
}
fun setStateWrapper(stateWrapper: StateWrapper?) {
mStateWrapper = stateWrapper
}
private fun updateInsets() {
val insets = mInsets
if (insets != null) {
val edges =
mEdges
?: SafeAreaViewEdges(
SafeAreaViewEdgeModes.ADDITIVE,
SafeAreaViewEdgeModes.ADDITIVE,
SafeAreaViewEdgeModes.ADDITIVE,
SafeAreaViewEdgeModes.ADDITIVE)
val stateWrapper = getStateWrapper()
if (stateWrapper != null) {
val map = Arguments.createMap()
map.putMap("insets", edgeInsetsToJsMap(insets))
stateWrapper.updateState(map)
} else {
val localData = SafeAreaViewLocalData(insets = insets, mode = mMode, edges = edges)
val reactContext = getReactContext(this)
val uiManager = reactContext.getNativeModule(UIManagerModule::class.java)
if (uiManager != null) {
uiManager.setViewLocalData(id, localData)
// Sadly there doesn't seem to be a way to properly dirty a yoga node from java, so if we
// are in
// the middle of a layout, we need to recompute it. There is also no way to know whether
// we
// are in the middle of a layout so always do it.
reactContext.runOnNativeModulesQueueThread {
uiManager.uiImplementation.dispatchViewUpdates(-1)
}
waitForReactLayout()
}
}
}
}
private fun waitForReactLayout() {
// Block the main thread until the native module thread is finished with
// its current tasks. To do this we use the done boolean as a lock and enqueue
// a task on the native modules thread. When the task runs we can unblock the
// main thread. This should be safe as long as the native modules thread
// does not block waiting on the main thread.
var done = false
val lock = ReentrantLock()
val condition = lock.newCondition()
val startTime = System.nanoTime()
var waitTime = 0L
getReactContext(this).runOnNativeModulesQueueThread {
lock.withLock {
if (!done) {
done = true
condition.signal()
}
}
}
lock.withLock {
while (!done && waitTime < MAX_WAIT_TIME_NANO) {
try {
condition.awaitNanos(MAX_WAIT_TIME_NANO)
} catch (ex: InterruptedException) {
// In case of an interrupt just give up waiting.
done = true
}
waitTime += System.nanoTime() - startTime
}
}
// Timed out waiting.
if (waitTime >= MAX_WAIT_TIME_NANO) {
Log.w("SafeAreaView", "Timed out waiting for layout.")
}
}
fun setMode(mode: SafeAreaViewMode) {
mMode = mode
updateInsets()
}
fun setEdges(edges: SafeAreaViewEdges) {
mEdges = edges
updateInsets()
}
private fun maybeUpdateInsets(): Boolean {
val providerView = mProviderView ?: return false
val edgeInsets = getSafeAreaInsets(providerView) ?: return false
if (mInsets != edgeInsets) {
mInsets = edgeInsets
updateInsets()
return true
}
return false
}
private fun findProvider(): View {
var current = parent
while (current != null) {
if (current is SafeAreaProvider) {
return current
}
current = current.parent
}
return this
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
mProviderView = findProvider()
mProviderView?.viewTreeObserver?.addOnPreDrawListener(this)
maybeUpdateInsets()
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
mProviderView?.viewTreeObserver?.removeOnPreDrawListener(this)
mProviderView = null
}
override fun onPreDraw(): Boolean {
val didUpdate = maybeUpdateInsets()
if (didUpdate) {
requestLayout()
}
return !didUpdate
}
}

View File

@@ -0,0 +1,16 @@
package com.th3rdwave.safeareacontext
enum class SafeAreaViewEdgeModes {
OFF,
ADDITIVE,
MAXIMUM
}
data class SafeAreaViewEdges(
val top: SafeAreaViewEdgeModes,
val right: SafeAreaViewEdgeModes,
val bottom: SafeAreaViewEdgeModes,
val left: SafeAreaViewEdgeModes
)
class Safe

View File

@@ -0,0 +1,7 @@
package com.th3rdwave.safeareacontext
data class SafeAreaViewLocalData(
val insets: EdgeInsets,
val mode: SafeAreaViewMode,
val edges: SafeAreaViewEdges,
)

View File

@@ -0,0 +1,67 @@
package com.th3rdwave.safeareacontext
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.ReactStylesDiffMap
import com.facebook.react.uimanager.StateWrapper
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.react.views.view.ReactViewManager
@ReactModule(name = SafeAreaViewManager.REACT_CLASS)
class SafeAreaViewManager : ReactViewManager() {
override fun getName() = REACT_CLASS
override fun createViewInstance(context: ThemedReactContext) = SafeAreaView(context)
override fun createShadowNodeInstance() = SafeAreaViewShadowNode()
override fun getShadowNodeClass() = SafeAreaViewShadowNode::class.java
@ReactProp(name = "mode")
fun setMode(view: SafeAreaView, mode: String?) {
when (mode) {
"padding" -> {
view.setMode(SafeAreaViewMode.PADDING)
}
"margin" -> {
view.setMode(SafeAreaViewMode.MARGIN)
}
}
}
@ReactProp(name = "edges")
fun setEdges(view: SafeAreaView, propList: ReadableMap?) {
if (propList != null) {
view.setEdges(
SafeAreaViewEdges(
top = propList.getString("top")?.let { SafeAreaViewEdgeModes.valueOf(it.uppercase()) }
?: SafeAreaViewEdgeModes.OFF,
right =
propList.getString("right")?.let { SafeAreaViewEdgeModes.valueOf(it.uppercase()) }
?: SafeAreaViewEdgeModes.OFF,
bottom =
propList.getString("bottom")?.let {
SafeAreaViewEdgeModes.valueOf(it.uppercase())
}
?: SafeAreaViewEdgeModes.OFF,
left =
propList.getString("left")?.let { SafeAreaViewEdgeModes.valueOf(it.uppercase()) }
?: SafeAreaViewEdgeModes.OFF))
}
}
override fun updateState(
view: ReactViewGroup,
props: ReactStylesDiffMap?,
stateWrapper: StateWrapper?
): Any? {
(view as SafeAreaView).setStateWrapper(stateWrapper)
return null
}
companion object {
const val REACT_CLASS = "RNCSafeAreaView"
}
}

View File

@@ -0,0 +1,6 @@
package com.th3rdwave.safeareacontext
enum class SafeAreaViewMode {
PADDING,
MARGIN
}

View File

@@ -0,0 +1,170 @@
package com.th3rdwave.safeareacontext
import com.facebook.react.bridge.Dynamic
import com.facebook.react.bridge.ReadableType
import com.facebook.react.uimanager.*
import com.facebook.react.uimanager.annotations.ReactPropGroup
import kotlin.math.max
class SafeAreaViewShadowNode : LayoutShadowNode() {
private var mLocalData: SafeAreaViewLocalData? = null
private val mPaddings: FloatArray = FloatArray(ViewProps.PADDING_MARGIN_SPACING_TYPES.size)
private val mMargins: FloatArray = FloatArray(ViewProps.PADDING_MARGIN_SPACING_TYPES.size)
private var mNeedsUpdate = false
init {
for (i in ViewProps.PADDING_MARGIN_SPACING_TYPES.indices) {
mPaddings[i] = Float.NaN
mMargins[i] = Float.NaN
}
}
private fun updateInsets() {
val localData = mLocalData ?: return
var top = 0f
var right = 0f
var bottom = 0f
var left = 0f
val meta = if (localData.mode == SafeAreaViewMode.PADDING) mPaddings else mMargins
val allEdges = meta[Spacing.ALL]
if (!java.lang.Float.isNaN(allEdges)) {
top = allEdges
right = allEdges
bottom = allEdges
left = allEdges
}
val verticalEdges = meta[Spacing.VERTICAL]
if (!java.lang.Float.isNaN(verticalEdges)) {
top = verticalEdges
bottom = verticalEdges
}
val horizontalEdges = meta[Spacing.HORIZONTAL]
if (!java.lang.Float.isNaN(horizontalEdges)) {
right = horizontalEdges
left = horizontalEdges
}
val topEdge = meta[Spacing.TOP]
if (!java.lang.Float.isNaN(topEdge)) {
top = topEdge
}
val rightEdge = meta[Spacing.RIGHT]
if (!java.lang.Float.isNaN(rightEdge)) {
right = rightEdge
}
val bottomEdge = meta[Spacing.BOTTOM]
if (!java.lang.Float.isNaN(bottomEdge)) {
bottom = bottomEdge
}
val leftEdge = meta[Spacing.LEFT]
if (!java.lang.Float.isNaN(leftEdge)) {
left = leftEdge
}
top = PixelUtil.toPixelFromDIP(top)
right = PixelUtil.toPixelFromDIP(right)
bottom = PixelUtil.toPixelFromDIP(bottom)
left = PixelUtil.toPixelFromDIP(left)
val edges = localData.edges
val insets = localData.insets
if (localData.mode == SafeAreaViewMode.PADDING) {
super.setPadding(Spacing.TOP, getEdgeValue(edges.top, insets.top, top))
super.setPadding(Spacing.RIGHT, getEdgeValue(edges.right, insets.right, right))
super.setPadding(Spacing.BOTTOM, getEdgeValue(edges.bottom, insets.bottom, bottom))
super.setPadding(Spacing.LEFT, getEdgeValue(edges.left, insets.left, left))
} else {
super.setMargin(Spacing.TOP, getEdgeValue(edges.top, insets.top, top))
super.setMargin(Spacing.RIGHT, getEdgeValue(edges.right, insets.right, right))
super.setMargin(Spacing.BOTTOM, getEdgeValue(edges.bottom, insets.bottom, bottom))
super.setMargin(Spacing.LEFT, getEdgeValue(edges.left, insets.left, left))
}
}
private fun getEdgeValue(
edgeMode: SafeAreaViewEdgeModes,
insetValue: Float,
edgeValue: Float
): Float {
if (edgeMode == SafeAreaViewEdgeModes.OFF) {
return edgeValue
} else if (edgeMode == SafeAreaViewEdgeModes.MAXIMUM) {
return max(insetValue, edgeValue)
} else {
return insetValue + edgeValue
}
}
private fun resetInsets(mode: SafeAreaViewMode) {
if (mode == SafeAreaViewMode.PADDING) {
super.setPadding(Spacing.TOP, mPaddings[Spacing.TOP])
super.setPadding(Spacing.RIGHT, mPaddings[Spacing.RIGHT])
super.setPadding(Spacing.BOTTOM, mPaddings[Spacing.BOTTOM])
super.setPadding(Spacing.LEFT, mPaddings[Spacing.LEFT])
} else {
super.setMargin(Spacing.TOP, mMargins[Spacing.TOP])
super.setMargin(Spacing.RIGHT, mMargins[Spacing.RIGHT])
super.setMargin(Spacing.BOTTOM, mMargins[Spacing.BOTTOM])
super.setMargin(Spacing.LEFT, mMargins[Spacing.LEFT])
}
markUpdated()
}
override fun onBeforeLayout(nativeViewHierarchyOptimizer: NativeViewHierarchyOptimizer) {
if (mNeedsUpdate) {
mNeedsUpdate = false
updateInsets()
}
}
override fun setLocalData(data: Any) {
if (data !is SafeAreaViewLocalData) {
return
}
val localData = mLocalData
if (localData != null && localData.mode != data.mode) {
resetInsets(localData.mode)
}
mLocalData = data
mNeedsUpdate = false
updateInsets()
}
// Names needs to reflect exact order in LayoutShadowNode.java
@ReactPropGroup(
names =
[
ViewProps.PADDING,
ViewProps.PADDING_VERTICAL,
ViewProps.PADDING_HORIZONTAL,
ViewProps.PADDING_START,
ViewProps.PADDING_END,
ViewProps.PADDING_TOP,
ViewProps.PADDING_BOTTOM,
ViewProps.PADDING_LEFT,
ViewProps.PADDING_RIGHT])
override fun setPaddings(index: Int, padding: Dynamic) {
val spacingType = ViewProps.PADDING_MARGIN_SPACING_TYPES[index]
mPaddings[spacingType] =
if (padding.type == ReadableType.Number) padding.asDouble().toFloat() else Float.NaN
super.setPaddings(index, padding)
mNeedsUpdate = true
}
@ReactPropGroup(
names =
[
ViewProps.MARGIN,
ViewProps.MARGIN_VERTICAL,
ViewProps.MARGIN_HORIZONTAL,
ViewProps.MARGIN_START,
ViewProps.MARGIN_END,
ViewProps.MARGIN_TOP,
ViewProps.MARGIN_BOTTOM,
ViewProps.MARGIN_LEFT,
ViewProps.MARGIN_RIGHT])
override fun setMargins(index: Int, margin: Dynamic) {
val spacingType = ViewProps.PADDING_MARGIN_SPACING_TYPES[index]
mMargins[spacingType] =
if (margin.type == ReadableType.Number) margin.asDouble().toFloat() else Float.NaN
super.setMargins(index, margin)
mNeedsUpdate = true
}
}

View File

@@ -0,0 +1,39 @@
package com.th3rdwave.safeareacontext
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.PixelUtil
fun edgeInsetsToJsMap(insets: EdgeInsets): WritableMap {
val insetsMap = Arguments.createMap()
insetsMap.putDouble("top", PixelUtil.toDIPFromPixel(insets.top).toDouble())
insetsMap.putDouble("right", PixelUtil.toDIPFromPixel(insets.right).toDouble())
insetsMap.putDouble("bottom", PixelUtil.toDIPFromPixel(insets.bottom).toDouble())
insetsMap.putDouble("left", PixelUtil.toDIPFromPixel(insets.left).toDouble())
return insetsMap
}
fun edgeInsetsToJavaMap(insets: EdgeInsets): Map<String, Float> {
return mapOf(
"top" to PixelUtil.toDIPFromPixel(insets.top),
"right" to PixelUtil.toDIPFromPixel(insets.right),
"bottom" to PixelUtil.toDIPFromPixel(insets.bottom),
"left" to PixelUtil.toDIPFromPixel(insets.left))
}
fun rectToJsMap(rect: Rect): WritableMap {
val rectMap = Arguments.createMap()
rectMap.putDouble("x", PixelUtil.toDIPFromPixel(rect.x).toDouble())
rectMap.putDouble("y", PixelUtil.toDIPFromPixel(rect.y).toDouble())
rectMap.putDouble("width", PixelUtil.toDIPFromPixel(rect.width).toDouble())
rectMap.putDouble("height", PixelUtil.toDIPFromPixel(rect.height).toDouble())
return rectMap
}
fun rectToJavaMap(rect: Rect): Map<String, Float> {
return mapOf(
"x" to PixelUtil.toDIPFromPixel(rect.x),
"y" to PixelUtil.toDIPFromPixel(rect.y),
"width" to PixelUtil.toDIPFromPixel(rect.width),
"height" to PixelUtil.toDIPFromPixel(rect.height))
}

View File

@@ -0,0 +1,87 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(LIB_LITERAL safeareacontext)
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})
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_nativemodule_core
react_render_core
react_render_debug
react_render_graphics
react_render_mapbuffer
react_render_componentregistry
react_utils
rrc_view
turbomodulejsijni
yoga
)
endif()
target_include_directories(
${CMAKE_PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 80)
target_compile_reactnative_options(${LIB_TARGET_NAME} PUBLIC)
else()
target_compile_options(
${LIB_TARGET_NAME}
PRIVATE
-fexceptions
-frtti
-std=c++20
-Wall
)
endif()
target_compile_options(
${LIB_TARGET_NAME}
PRIVATE
-Wpedantic
-Wno-gnu-zero-variadic-macro-arguments
-Wno-dollar-in-identifier-extension
)

View File

@@ -0,0 +1,17 @@
#pragma once
#include <ReactCommon/JavaTurboModule.h>
#include <ReactCommon/TurboModule.h>
#include <jsi/jsi.h>
#include <react/renderer/components/safeareacontext/RNCSafeAreaViewComponentDescriptor.h>
namespace facebook {
namespace react {
JSI_EXPORT
std::shared_ptr<TurboModule> safeareacontext_ModuleProvider(
const std::string &moduleName,
const JavaTurboModule::InitParams &params);
} // namespace react
} // namespace facebook

View File

@@ -0,0 +1,31 @@
/**
* This code was generated by
* [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* <p>Do not edit this file as changes may cause incorrect behavior and will be lost once the code
* is regenerated.
*
* @generated by codegen project: GeneratePropsJavaDelegate.js
*/
package com.facebook.react.viewmanagers;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.uimanager.BaseViewManager;
import com.facebook.react.uimanager.BaseViewManagerDelegate;
import com.facebook.react.uimanager.LayoutShadowNode;
public class RNCSafeAreaProviderManagerDelegate<
T extends View,
U extends
BaseViewManager<T, ? extends LayoutShadowNode> & RNCSafeAreaProviderManagerInterface<T>>
extends BaseViewManagerDelegate<T, U> {
public RNCSafeAreaProviderManagerDelegate(U viewManager) {
super(viewManager);
}
@Override
public void setProperty(T view, String propName, @Nullable Object value) {
super.setProperty(view, propName, value);
}
}

View File

@@ -0,0 +1,16 @@
/**
* This code was generated by
* [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* <p>Do not edit this file as changes may cause incorrect behavior and will be lost once the code
* is regenerated.
*
* @generated by codegen project: GeneratePropsJavaInterface.js
*/
package com.facebook.react.viewmanagers;
import android.view.View;
public interface RNCSafeAreaProviderManagerInterface<T extends View> {
// No props
}

View File

@@ -0,0 +1,41 @@
/**
* This code was generated by
* [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* <p>Do not edit this file as changes may cause incorrect behavior and will be lost once the code
* is regenerated.
*
* @generated by codegen project: GeneratePropsJavaDelegate.js
*/
package com.facebook.react.viewmanagers;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.BaseViewManager;
import com.facebook.react.uimanager.BaseViewManagerDelegate;
import com.facebook.react.uimanager.LayoutShadowNode;
public class RNCSafeAreaViewManagerDelegate<
T extends View,
U extends
BaseViewManager<T, ? extends LayoutShadowNode> & RNCSafeAreaViewManagerInterface<T>>
extends BaseViewManagerDelegate<T, U> {
public RNCSafeAreaViewManagerDelegate(U viewManager) {
super(viewManager);
}
@Override
public void setProperty(T view, String propName, @Nullable Object value) {
switch (propName) {
case "mode":
mViewManager.setMode(view, (String) value);
break;
case "edges":
mViewManager.setEdges(view, (ReadableMap) value);
break;
default:
super.setProperty(view, propName, value);
}
}
}

View File

@@ -0,0 +1,20 @@
/**
* This code was generated by
* [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* <p>Do not edit this file as changes may cause incorrect behavior and will be lost once the code
* is regenerated.
*
* @generated by codegen project: GeneratePropsJavaInterface.js
*/
package com.facebook.react.viewmanagers;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReadableMap;
public interface RNCSafeAreaViewManagerInterface<T extends View> {
void setMode(T view, @Nullable String value);
void setEdges(T view, @Nullable ReadableMap value);
}

View File

@@ -0,0 +1,29 @@
@file:Suppress("DEPRECATION")
package com.th3rdwave.safeareacontext
import com.facebook.react.bridge.Arguments
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter
internal class InsetsChangeEvent(
@Suppress("UNUSED_PARAMETER") surfaceId: Int,
viewTag: Int,
private val mInsets: EdgeInsets,
private val mFrame: Rect
// New ctor is only available in RN 0.65.
) : Event<InsetsChangeEvent>(viewTag) {
override fun getEventName() = EVENT_NAME
// TODO: Migrate to getEventData when dropping support for RN 0.64.
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
val event = Arguments.createMap()
event.putMap("insets", edgeInsetsToJsMap(mInsets))
event.putMap("frame", rectToJsMap(mFrame))
rctEventEmitter.receiveEvent(viewTag, eventName, event)
}
companion object {
const val EVENT_NAME = "topInsetsChange"
}
}

View File

@@ -0,0 +1,56 @@
/**
* This code was generated by
* [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* <p>Do not edit this file as changes may cause incorrect behavior and will be lost once the code
* is regenerated.
*
* @generated by codegen project: GenerateModuleJavaSpec.js
* @nolint
*/
package com.th3rdwave.safeareacontext;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactModuleWithSpec;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
public abstract class NativeSafeAreaContextSpec extends ReactContextBaseJavaModule
implements ReactModuleWithSpec, TurboModule {
public NativeSafeAreaContextSpec(ReactApplicationContext reactContext) {
super(reactContext);
}
protected abstract Map<String, Object> getTypedExportedConstants();
@Override
@DoNotStrip
public final @Nullable Map<String, Object> getConstants() {
Map<String, Object> constants = getTypedExportedConstants();
if (ReactBuildConfig.DEBUG || ReactBuildConfig.IS_INTERNAL_BUILD) {
Set<String> obligatoryFlowConstants = new HashSet<>();
Set<String> optionalFlowConstants = new HashSet<>(Arrays.asList("initialWindowMetrics"));
Set<String> undeclaredConstants = new HashSet<>(constants.keySet());
undeclaredConstants.removeAll(obligatoryFlowConstants);
undeclaredConstants.removeAll(optionalFlowConstants);
if (!undeclaredConstants.isEmpty()) {
throw new IllegalStateException(
String.format("Native Module Flow doesn't declare constants: %s", undeclaredConstants));
}
undeclaredConstants = obligatoryFlowConstants;
undeclaredConstants.removeAll(constants.keySet());
if (!undeclaredConstants.isEmpty()) {
throw new IllegalStateException(
String.format("Native Module doesn't fill in constants: %s", undeclaredConstants));
}
}
return constants;
}
}

View File

@@ -0,0 +1,20 @@
package com.th3rdwave.safeareacontext
import android.content.Context
import android.content.ContextWrapper
import android.view.View
import com.facebook.react.bridge.ReactContext
/** UIManagerHelper.getReactContext only exists in RN 0.63+ so vendor it here for a while. */
fun getReactContext(view: View): ReactContext {
var context = view.context
if (context !is ReactContext && context is ContextWrapper) {
context = context.baseContext
}
return context as ReactContext
}
/** UIManagerHelper.getSurfaceId only exists in RN 0.65+, surface id is only needed for new arch. */
fun getSurfaceId(@Suppress("UNUSED_PARAMETER") context: Context): Int {
return -1
}