diff --git a/android/src/main/java/com/unistyles/Platform.kt b/android/src/main/java/com/unistyles/Platform.kt index 67a13734..045c924e 100644 --- a/android/src/main/java/com/unistyles/Platform.kt +++ b/android/src/main/java/com/unistyles/Platform.kt @@ -73,7 +73,7 @@ class Platform(private val reactApplicationContext: ReactApplicationContext) { return contentSizeCategory } - fun setInsetsCompat(insetsCompat: WindowInsetsCompat, window: Window) { + fun setInsetsCompat(insetsCompat: WindowInsetsCompat, window: Window, animatedBottomInsets: Int?) { // below Android 11, we need to use window flags to detect status bar visibility val isStatusBarVisible = when(Build.VERSION.SDK_INT) { in 30..Int.MAX_VALUE -> { @@ -98,13 +98,24 @@ class Platform(private val reactApplicationContext: ReactApplicationContext) { } val insets = insetsCompat.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()) - - // reports inset bottom as 0 when keyboard is visible - // otherwise it will break other libraries that manipulates bottom insets val imeInsets = insetsCompat.getInsets(WindowInsetsCompat.Type.ime()) - val insetsBottom = if (imeInsets.bottom > 0) 0 else insets.bottom - this.insets = Insets(statusBarTopInset, insetsBottom, insets.left, insets.right) + // Android 10 and below - set bottom insets to 0 while keyboard is visible and use default bottom insets otherwise + // Android 11 and above - animate bottom insets while keyboard is appearing and disappearing + val insetBottom = when(imeInsets.bottom > 0) { + true -> { + if (Build.VERSION.SDK_INT >= 30 && animatedBottomInsets != null) { + animatedBottomInsets + } else { + 0 + } + } + else -> { + insets.bottom + } + } + + this.insets = Insets(statusBarTopInset, insetBottom, insets.left, insets.right) } fun getInsets(): Insets { diff --git a/android/src/main/java/com/unistyles/UnistylesModule.kt b/android/src/main/java/com/unistyles/UnistylesModule.kt index 8041dc57..b4986e44 100644 --- a/android/src/main/java/com/unistyles/UnistylesModule.kt +++ b/android/src/main/java/com/unistyles/UnistylesModule.kt @@ -4,17 +4,21 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.os.Build import android.os.Handler import android.os.Looper import android.util.Log import android.view.View import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsAnimationCompat +import androidx.core.view.WindowInsetsCompat import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.turbomodule.core.interfaces.CallInvokerHolder +import kotlin.math.roundToInt class UnistylesModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener { private var isCxxReady: Boolean = false @@ -141,7 +145,7 @@ class UnistylesModule(reactContext: ReactApplicationContext) : ReactContextBaseJ this.reactApplicationContext.currentActivity?.let { activity -> activity.findViewById(android.R.id.content)?.let { mainView -> ViewCompat.setOnApplyWindowInsetsListener(mainView) { _, insets -> - this.platform.setInsetsCompat(insets, activity.window) + this.platform.setInsetsCompat(insets, activity.window, null) if (this.isCxxReady) { this.onLayoutConfigChange() @@ -149,6 +153,53 @@ class UnistylesModule(reactContext: ReactApplicationContext) : ReactContextBaseJ insets } + + if (Build.VERSION.SDK_INT >= 30) { + ViewCompat.setWindowInsetsAnimationCallback( + mainView, + object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { + var initialBottomInsets = 0 + var isGoingUp = false + + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + val insets = ViewCompat.getRootWindowInsets(mainView) + val isKeyboardVisible = insets?.isVisible(WindowInsetsCompat.Type.ime()) ?: false + + if (!isKeyboardVisible) { + val density = reactApplicationContext.resources.displayMetrics.density + + initialBottomInsets = (this@UnistylesModule.platform.getInsets().bottom * density).roundToInt() + } + + isGoingUp = !isKeyboardVisible + } + + override fun onProgress( + insets: WindowInsetsCompat, + runningAnimations: List + ): WindowInsetsCompat { + runningAnimations.firstOrNull()?.let { animation -> + val progress = animation.fraction + val nextBottomInset = if (isGoingUp) { + (initialBottomInsets - (progress * initialBottomInsets).roundToInt()) + } else { + // enable this in Unistyles 3.0 to get real time bottom insets + // initialBottomInsets - (initialBottomInsets - (progress * initialBottomInsets).roundToInt()) + initialBottomInsets + } + + this@UnistylesModule.platform.setInsetsCompat(insets, activity.window, nextBottomInset) + + if (!isGoingUp) { + this@UnistylesModule.onLayoutConfigChange() + } + } + + return insets + } + } + ) + } } } } @@ -157,6 +208,7 @@ class UnistylesModule(reactContext: ReactApplicationContext) : ReactContextBaseJ this.reactApplicationContext.currentActivity?.let { activity -> activity.window?.decorView?.let { view -> ViewCompat.setOnApplyWindowInsetsListener(view, null) + ViewCompat.setWindowInsetsAnimationCallback(view, null) } } } diff --git a/examples/expo/src/App.tsx b/examples/expo/src/App.tsx index da5c1c9c..365574ba 100644 --- a/examples/expo/src/App.tsx +++ b/examples/expo/src/App.tsx @@ -58,6 +58,7 @@ export const App: React.FunctionComponent = () => ( + diff --git a/examples/expo/src/common/navigation.ts b/examples/expo/src/common/navigation.ts index e66e00ba..4d599533 100644 --- a/examples/expo/src/common/navigation.ts +++ b/examples/expo/src/common/navigation.ts @@ -30,7 +30,8 @@ export enum DemoNames { BooleanVariants = 'BooleanVariantsScreen', UpdateTheme = 'UpdateThemeScreen', AndroidStatusBarNavigationBar = 'AndroidStatusBarNavigationBarScreen', - Layout = 'LayoutScreen' + Layout = 'LayoutScreen', + Keyboard = 'KeyboardScreen' } export type DemoStackParams = { @@ -63,7 +64,8 @@ export type DemoStackParams = { [DemoNames.BooleanVariants]: undefined, [DemoNames.UpdateTheme]: undefined, [DemoNames.AndroidStatusBarNavigationBar]: undefined, - [DemoNames.Layout]: undefined + [DemoNames.Layout]: undefined, + [DemoNames.Keyboard]: undefined } export type NavigationProps = NavigationProp diff --git a/examples/expo/src/examples/HomeScreen.tsx b/examples/expo/src/examples/HomeScreen.tsx index db4b036c..b90d046a 100644 --- a/examples/expo/src/examples/HomeScreen.tsx +++ b/examples/expo/src/examples/HomeScreen.tsx @@ -490,6 +490,23 @@ export const HomeScreen = () => { navigation.navigate(DemoNames.AndroidStatusBarNavigationBar) }} /> + { + UnistylesRegistry + .addThemes({ + light: lightTheme, + dark: darkTheme, + premium: premiumTheme + }) + .addBreakpoints(breakpoints) + .addConfig({ + initialTheme: 'light' + }) + + navigation.navigate(DemoNames.Keyboard) + }} + /> { + const { styles } = useStyles(stylesheet) + + return ( + + + + + + ) +} + +const stylesheet = createStyleSheet((theme, rt) => { + console.log(rt.insets) + + return { + container: { + flex: 1, + justifyContent: 'flex-end', + alignItems: 'center', + paddingHorizontal: 20, + backgroundColor: theme.colors.backgroundColor, + rowGap: 20 + }, + textInput: { + width: '100%', + height: 50, + borderWidth: 1, + borderColor: theme.colors.accent, + paddingHorizontal: 10, + marginBottom: rt.insets.bottom + 20 + } + } +}) diff --git a/examples/expo/src/examples/index.ts b/examples/expo/src/examples/index.ts index ffdd3188..ebb135b2 100644 --- a/examples/expo/src/examples/index.ts +++ b/examples/expo/src/examples/index.ts @@ -28,3 +28,4 @@ export { BooleanVariantsScreen } from './BooleanVariantsScreen' export { UpdateThemeScreen } from './UpdateThemeScreen' export { AndroidStatusBarNavigationBarScreen } from './AndroidStatusBarNavigationBarScreen' export { LayoutScreen } from './LayoutScreen' +export { KeyboardScreen } from './KeyboardScreen'