Skip to content

Commit

Permalink
feat: animate bottom insets android
Browse files Browse the repository at this point in the history
  • Loading branch information
jpudysz committed Jul 15, 2024
1 parent 9e60a0a commit bd9c4a1
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 9 deletions.
23 changes: 17 additions & 6 deletions android/src/main/java/com/unistyles/Platform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
Expand All @@ -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 {
Expand Down
54 changes: 53 additions & 1 deletion android/src/main/java/com/unistyles/UnistylesModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -141,14 +145,61 @@ class UnistylesModule(reactContext: ReactApplicationContext) : ReactContextBaseJ
this.reactApplicationContext.currentActivity?.let { activity ->
activity.findViewById<View>(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()
}

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<WindowInsetsAnimationCompat>
): 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
}
}
)
}
}
}
}
Expand All @@ -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)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions examples/expo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const App: React.FunctionComponent = () => (
<Stack.Screen name={DemoNames.BooleanVariants} component={Screens.BooleanVariantsScreen} />
<Stack.Screen name={DemoNames.UpdateTheme} component={Screens.UpdateThemeScreen} />
<Stack.Screen name={DemoNames.AndroidStatusBarNavigationBar} component={Screens.AndroidStatusBarNavigationBarScreen} />
<Stack.Screen name={DemoNames.Keyboard} component={Screens.KeyboardScreen} />
</Stack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
Expand Down
6 changes: 4 additions & 2 deletions examples/expo/src/common/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export enum DemoNames {
BooleanVariants = 'BooleanVariantsScreen',
UpdateTheme = 'UpdateThemeScreen',
AndroidStatusBarNavigationBar = 'AndroidStatusBarNavigationBarScreen',
Layout = 'LayoutScreen'
Layout = 'LayoutScreen',
Keyboard = 'KeyboardScreen'
}

export type DemoStackParams = {
Expand Down Expand Up @@ -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<S extends DemoNames = DemoNames.Home> = NavigationProp<DemoStackParams, S>
17 changes: 17 additions & 0 deletions examples/expo/src/examples/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,23 @@ export const HomeScreen = () => {
navigation.navigate(DemoNames.AndroidStatusBarNavigationBar)
}}
/>
<DemoLink
description="Keyboard screen"
onPress={() => {
UnistylesRegistry
.addThemes({
light: lightTheme,
dark: darkTheme,
premium: premiumTheme
})
.addBreakpoints(breakpoints)
.addConfig({
initialTheme: 'light'
})

navigation.navigate(DemoNames.Keyboard)
}}
/>
</DemoGroup>
<DemoGroup title="Benchmark">
<DemoLink
Expand Down
45 changes: 45 additions & 0 deletions examples/expo/src/examples/KeyboardScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react'
import { KeyboardAvoidingView, TextInput } from 'react-native'
import { createStyleSheet, useStyles } from 'react-native-unistyles'
import { DemoScreen } from '../components'

export const KeyboardScreen: React.FunctionComponent = () => {
const { styles } = useStyles(stylesheet)

return (
<DemoScreen>
<KeyboardAvoidingView
style={styles.container}
behavior="padding"
>
<TextInput
placeholder="Type something"
style={styles.textInput}
/>
</KeyboardAvoidingView>
</DemoScreen>
)
}

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
}
}
})
1 change: 1 addition & 0 deletions examples/expo/src/examples/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ export { BooleanVariantsScreen } from './BooleanVariantsScreen'
export { UpdateThemeScreen } from './UpdateThemeScreen'
export { AndroidStatusBarNavigationBarScreen } from './AndroidStatusBarNavigationBarScreen'
export { LayoutScreen } from './LayoutScreen'
export { KeyboardScreen } from './KeyboardScreen'

0 comments on commit bd9c4a1

Please sign in to comment.