Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ android {
}
}

compileSdkVersion getExtOrDefault('compileSdkVersion', 30)
compileSdkVersion getExtOrDefault('compileSdkVersion', 31)

// 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@ internal class InsetsChangeEvent(
surfaceId: Int,
viewTag: Int,
private val mInsets: EdgeInsets,
private val mVerticalCornerAdaptedInsets: EdgeInsets,
private val mHorizontalCornerAdaptedInsets: 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("verticalCornerAdaptedInsets", edgeInsetsToJsMap(mVerticalCornerAdaptedInsets))
event.putMap(
"horizontalCornerAdaptedInsets",
edgeInsetsToJsMap(mHorizontalCornerAdaptedInsets),
)
event.putMap("frame", rectToJsMap(mFrame))
return event
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,22 @@ class SafeAreaContextModule(reactContext: ReactApplicationContext?) :
val contentView = decorView?.findViewById<View>(android.R.id.content) ?: return null
val insets = getSafeAreaInsets(decorView)
val frame = getFrame(decorView, contentView)
val corners = getRoundedCorners(decorView)
return if (insets == null || frame == null) {
null
} else mapOf("insets" to edgeInsetsToJavaMap(insets), "frame" to rectToJavaMap(frame))
} else
mapOf(
"insets" to edgeInsetsToJavaMap(insets),
"verticalCornerAdaptedInsets" to
edgeInsetsToJavaMap(
getCornerAdaptedInsets(decorView, insets, corners, CornerAdaptation.VERTICAL)
),
"horizontalCornerAdaptedInsets" to
edgeInsetsToJavaMap(
getCornerAdaptedInsets(decorView, insets, corners, CornerAdaptation.HORIZONTAL)
),
"frame" to rectToJavaMap(frame),
)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.th3rdwave.safeareacontext

import android.os.Build
import android.view.RoundedCorner
import android.view.View
import androidx.annotation.RequiresApi
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt

enum class CornerAdaptation {
NONE,
VERTICAL,
HORIZONTAL,
}

/**
* Enlarges [insets] of [view] where the display's rounded corners reach past them, matching UIKit's
* `edgeInsets(for: .safeArea(cornerAdaptation:))` on iOS 26.
*
* Measured on iOS 26/27 simulators, UIKit gives each corner the inset `r * (1 - √2 / 2)` (the arc's
* 45° point) of an effective radius `r = min(R - max(dx, dy), width / 2, height / 2)`, where `R` is
* the display corner radius and `dx`, `dy` are the view's distances from the two edges meeting at
* that corner. Vertical adaptation applies the top corners to the top inset and the bottom corners
* to the bottom inset, horizontal applies the left and right corners to the side insets, each never
* below the regular inset. [corners] comes from [getRoundedCorners]; without it the insets are
* returned unchanged.
*/
fun getCornerAdaptedInsets(
view: View,
insets: EdgeInsets,
corners: List<RoundedCorner?>?,
cornerAdaptation: CornerAdaptation,
): EdgeInsets {
if (
cornerAdaptation == CornerAdaptation.NONE ||
corners == null ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.S
) {
return insets
}
return getCornerAdaptedInsetsS(view, insets, corners, cornerAdaptation)
}

/**
* The display's rounded corners for [view]'s window, indexed by [RoundedCorner] position, or null
* before API 31 where [WindowInsets.getRoundedCorner] doesn't exist.
*/
fun getRoundedCorners(view: View): List<RoundedCorner?>? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return null
}
return getRoundedCornersS(view)
}

@RequiresApi(Build.VERSION_CODES.S)
private fun getRoundedCornersS(view: View): List<RoundedCorner?>? {
val windowInsets = view.rootWindowInsets ?: return null
return List(4) { windowInsets.getRoundedCorner(it) }
}

private val ARC_45_DEGREES = 1f - sqrt(2f) / 2f

@RequiresApi(Build.VERSION_CODES.S)
private fun getCornerAdaptedInsetsS(
view: View,
insets: EdgeInsets,
corners: List<RoundedCorner?>,
cornerAdaptation: CornerAdaptation,
): EdgeInsets {
val location = IntArray(2)
view.getLocationInWindow(location)
val left = location[0].toFloat()
val top = location[1].toFloat()
val right = left + view.width
val bottom = top + view.height
val maxRadius = min(view.width, view.height) / 2f

fun cornerInset(position: Int): Float {
val corner = corners[position] ?: return 0f
val radius = corner.radius.toFloat()
val center = corner.center
val dx =
when (position) {
RoundedCorner.POSITION_TOP_LEFT,
RoundedCorner.POSITION_BOTTOM_LEFT -> left - (center.x - radius)
else -> center.x + radius - right
}
val dy =
when (position) {
RoundedCorner.POSITION_TOP_LEFT,
RoundedCorner.POSITION_TOP_RIGHT -> top - (center.y - radius)
else -> center.y + radius - bottom
}
val effectiveRadius = min(radius - max(dx, dy), maxRadius)
return max(effectiveRadius, 0f).times(ARC_45_DEGREES).roundToInt().toFloat()
}

return if (cornerAdaptation == CornerAdaptation.VERTICAL) {
insets.copy(
top =
max(
insets.top,
max(
cornerInset(RoundedCorner.POSITION_TOP_LEFT),
cornerInset(RoundedCorner.POSITION_TOP_RIGHT),
),
),
bottom =
max(
insets.bottom,
max(
cornerInset(RoundedCorner.POSITION_BOTTOM_LEFT),
cornerInset(RoundedCorner.POSITION_BOTTOM_RIGHT),
),
),
)
} else {
insets.copy(
left =
max(
insets.left,
max(
cornerInset(RoundedCorner.POSITION_TOP_LEFT),
cornerInset(RoundedCorner.POSITION_BOTTOM_LEFT),
),
),
right =
max(
insets.right,
max(
cornerInset(RoundedCorner.POSITION_TOP_RIGHT),
cornerInset(RoundedCorner.POSITION_BOTTOM_RIGHT),
),
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ package com.th3rdwave.safeareacontext

import android.content.Context
import android.os.Build
import android.view.RoundedCorner
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.WindowInsets
import com.facebook.react.views.view.ReactViewGroup

typealias OnInsetsChangeHandler = (view: SafeAreaProvider, insets: EdgeInsets, frame: Rect) -> Unit
typealias OnInsetsChangeHandler =
(
view: SafeAreaProvider,
insets: EdgeInsets,
verticalCornerAdaptedInsets: EdgeInsets,
horizontalCornerAdaptedInsets: 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 var mLastCorners: List<RoundedCorner?>? = null
private val mDispatchedInsets = DispatchedWindowInsets()

internal fun getDispatchedWindowInsets(): WindowInsets? = mDispatchedInsets.get(this)
Expand All @@ -22,10 +31,18 @@ class SafeAreaProvider(context: Context?) :
val insetsChangeHandler = mInsetsChangeHandler ?: return
val edgeInsets = getSafeAreaInsets(this, getDispatchedWindowInsets()) ?: return
val frame = getFrame(rootView as ViewGroup, this) ?: return
if (mLastInsets != edgeInsets || mLastFrame != frame) {
insetsChangeHandler(this, edgeInsets, frame)
val corners = getRoundedCorners(this)
if (mLastInsets != edgeInsets || mLastFrame != frame || mLastCorners != corners) {
insetsChangeHandler(
this,
edgeInsets,
getCornerAdaptedInsets(this, edgeInsets, corners, CornerAdaptation.VERTICAL),
getCornerAdaptedInsets(this, edgeInsets, corners, CornerAdaptation.HORIZONTAL),
frame,
)
mLastInsets = edgeInsets
mLastFrame = frame
mLastCorners = corners
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,24 @@ class SafeAreaProviderManager :
}
}

private fun handleOnInsetsChange(view: SafeAreaProvider, insets: EdgeInsets, frame: Rect) {
private fun handleOnInsetsChange(
view: SafeAreaProvider,
insets: EdgeInsets,
verticalCornerAdaptedInsets: EdgeInsets,
horizontalCornerAdaptedInsets: EdgeInsets,
frame: Rect,
) {
val reactContext = view.context as ReactContext
val reactTag = view.id
UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag)
?.dispatchEvent(InsetsChangeEvent(getSurfaceId(reactContext), reactTag, insets, frame))
?.dispatchEvent(
InsetsChangeEvent(
getSurfaceId(reactContext),
reactTag,
insets,
verticalCornerAdaptedInsets,
horizontalCornerAdaptedInsets,
frame,
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.th3rdwave.safeareacontext

import android.content.Context
import android.os.Build
import android.view.RoundedCorner
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.view.WindowInsets
import com.facebook.react.bridge.Arguments
Expand All @@ -15,6 +17,10 @@ class SafeAreaView(context: Context?) :
private var mMode = SafeAreaViewMode.PADDING
private var mInsets: EdgeInsets? = null
private var mEdges: SafeAreaViewEdges? = null
private var mCornerAdaptation = CornerAdaptation.NONE
private var mLastSafeAreaInsets: EdgeInsets? = null
private var mLastProviderFrame: Rect? = null
private var mLastCorners: List<RoundedCorner?>? = null
private var mProviderView: View? = null
private var mStateWrapper: StateWrapper? = null
private val mDispatchedInsets = DispatchedWindowInsets()
Expand Down Expand Up @@ -64,12 +70,36 @@ class SafeAreaView(context: Context?) :
updateInsets()
}

fun setCornerAdaptation(cornerAdaptation: CornerAdaptation) {
mCornerAdaptation = cornerAdaptation
mLastSafeAreaInsets = null
maybeUpdateInsets()
}

private fun maybeUpdateInsets(): Boolean {
val providerView = mProviderView ?: return false
val dispatchedInsets =
if (providerView is SafeAreaProvider) providerView.getDispatchedWindowInsets()
else mDispatchedInsets.get(this)
val edgeInsets = getSafeAreaInsets(providerView, dispatchedInsets) ?: return false
val safeAreaInsets = getSafeAreaInsets(providerView, dispatchedInsets) ?: return false
val edgeInsets: EdgeInsets
if (mCornerAdaptation == CornerAdaptation.NONE) {
edgeInsets = safeAreaInsets
} else {
val frame = getFrame(providerView.rootView as ViewGroup, providerView) ?: return false
val corners = getRoundedCorners(providerView)
if (
mLastSafeAreaInsets == safeAreaInsets &&
mLastProviderFrame == frame &&
mLastCorners == corners
) {
return false
}
mLastSafeAreaInsets = safeAreaInsets
mLastProviderFrame = frame
mLastCorners = corners
edgeInsets = getCornerAdaptedInsets(providerView, safeAreaInsets, corners, mCornerAdaptation)
}
if (mInsets != edgeInsets) {
mInsets = edgeInsets
updateInsets()
Expand Down Expand Up @@ -103,6 +133,7 @@ class SafeAreaView(context: Context?) :
super.onDetachedFromWindow()
mProviderView?.viewTreeObserver?.removeOnPreDrawListener(this)
mProviderView = null
mLastSafeAreaInsets = null
mDispatchedInsets.clear()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ class SafeAreaViewManager : ReactViewManager() {
}
}

@ReactProp(name = "cornerAdaptation")
fun setCornerAdaptation(view: SafeAreaView, cornerAdaptation: String?) {
view.setCornerAdaptation(
when (cornerAdaptation) {
"vertical" -> CornerAdaptation.VERTICAL
"horizontal" -> CornerAdaptation.HORIZONTAL
else -> CornerAdaptation.NONE
}
)
}

override fun updateState(
view: ReactViewGroup,
props: ReactStylesDiffMap?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case "edges":
mViewManager.setEdges(view, (ReadableMap) value);
break;
case "cornerAdaptation":
mViewManager.setCornerAdaptation(view, (String) value);
break;
default:
super.setProperty(view, propName, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public interface RNCSafeAreaViewManagerInterface<T extends View> {
void setMode(T view, @Nullable String value);

void setEdges(T view, @Nullable ReadableMap value);

void setCornerAdaptation(T view, @Nullable String value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ internal class InsetsChangeEvent(
@Suppress("UNUSED_PARAMETER") surfaceId: Int,
viewTag: Int,
private val mInsets: EdgeInsets,
private val mVerticalCornerAdaptedInsets: EdgeInsets,
private val mHorizontalCornerAdaptedInsets: EdgeInsets,
private val mFrame: Rect,
// New ctor is only available in RN 0.65.
) : Event<InsetsChangeEvent>(viewTag) {
Expand All @@ -19,6 +21,11 @@ internal class InsetsChangeEvent(
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
val event = Arguments.createMap()
event.putMap("insets", edgeInsetsToJsMap(mInsets))
event.putMap("verticalCornerAdaptedInsets", edgeInsetsToJsMap(mVerticalCornerAdaptedInsets))
event.putMap(
"horizontalCornerAdaptedInsets",
edgeInsetsToJsMap(mHorizontalCornerAdaptedInsets),
)
event.putMap("frame", rectToJsMap(mFrame))
rctEventEmitter.receiveEvent(viewTag, eventName, event)
}
Expand Down
Loading
Loading