Skip to content

Commit

Permalink
Synchronize IME insets with iOS keyboard
Browse files Browse the repository at this point in the history
  • Loading branch information
terrakok committed Oct 13, 2023
1 parent 3c84346 commit 4739ce3
Showing 1 changed file with 100 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import androidx.compose.ui.input.pointer.HistoricalChange
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerType
import androidx.compose.ui.input.pointer.toCompose
import androidx.compose.ui.interop.LocalLayerContainer
import androidx.compose.ui.interop.LocalUIKitInteropContext
import androidx.compose.ui.interop.LocalUIViewController
Expand All @@ -43,37 +42,37 @@ import androidx.compose.ui.platform.*
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.uikit.*
import androidx.compose.ui.unit.*
import androidx.compose.ui.util.fastMap
import kotlin.math.floor
import kotlin.math.roundToInt
import kotlin.math.roundToLong
import kotlin.math.roundToLong
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExportObjCClass
import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.objcPtr
import kotlinx.cinterop.readValue
import kotlinx.cinterop.useContents
import kotlinx.coroutines.Dispatchers
import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.OS
import org.jetbrains.skiko.OSVersion
import org.jetbrains.skiko.SkikoKeyboardEvent
import org.jetbrains.skiko.SkikoPointerEvent
import org.jetbrains.skiko.currentNanoTime
import platform.CoreGraphics.CGPoint
import org.jetbrains.skiko.available
import platform.CoreGraphics.CGAffineTransformIdentity
import platform.CoreGraphics.CGAffineTransformInvert
import platform.CoreGraphics.CGFloat
import platform.CoreGraphics.CGPoint
import platform.CoreGraphics.CGPointMake
import platform.CoreGraphics.CGRectMake
import platform.CoreGraphics.CGSize
import platform.CoreGraphics.CGSizeEqualToSize
import platform.Foundation.*
import platform.QuartzCore.CADisplayLink
import platform.QuartzCore.CATransaction
import platform.QuartzCore.kCATransactionDisableActions
import platform.UIKit.*
import platform.darwin.NSObject
import platform.darwin.dispatch_async
import platform.darwin.dispatch_get_main_queue
import platform.darwin.sel_registerName

private val uiContentSizeCategoryToFontScaleMap = mapOf(
UIContentSizeCategoryExtraSmall to 0.8f,
Expand Down Expand Up @@ -162,6 +161,15 @@ internal actual class ComposeWindow : UIViewController {
private var safeAreaState by mutableStateOf(PlatformInsets())
private var layoutMarginsState by mutableStateOf(PlatformInsets())

//invisible view to track system keyboard animation
private val keyboardAnimationView: UIView by lazy {
UIView.new()!!.apply {
setFrame(CGRectMake(0.0, 0.0, 0.0, 0.0))
hidden = true
}
}
private var keyboardAnimationListener: CADisplayLink? = null

/*
* Initial value is arbitarily chosen to avoid propagating invalid value logic
* It's never the case in real usage scenario to reflect that in type system
Expand Down Expand Up @@ -225,22 +233,12 @@ internal actual class ComposeWindow : UIViewController {
@Suppress("unused")
@ObjCAction
fun keyboardWillShow(arg: NSNotification) {
val keyboardInfo = arg.userInfo!!["UIKeyboardFrameEndUserInfoKey"] as NSValue
val keyboardHeight = keyboardInfo.CGRectValue().useContents { size.height }
val screenHeight = UIScreen.mainScreen.bounds.useContents { size.height }

val composeViewBottomY = UIScreen.mainScreen.coordinateSpace.convertPoint(
point = CGPointMake(0.0, view.frame.useContents { size.height }),
fromCoordinateSpace = view.coordinateSpace
).useContents { y }
val bottomIndent = screenHeight - composeViewBottomY

if (bottomIndent < keyboardHeight) {
keyboardOverlapHeightState = (keyboardHeight - bottomIndent).toFloat()
}
animateKeyboard(arg, true)

val scene = attachedComposeContext?.scene ?: return

val userInfo = arg.userInfo ?: return
val keyboardInfo = userInfo[UIKeyboardFrameEndUserInfoKey] as NSValue
val keyboardHeight = keyboardInfo.CGRectValue().useContents { size.height }
if (configuration.onFocusBehavior == OnFocusBehavior.FocusableAboveKeyboard) {
val focusedRect = scene.mainOwner?.focusOwner?.getFocusRect()?.toDpRect(density)

Expand All @@ -255,12 +253,92 @@ internal actual class ComposeWindow : UIViewController {
@Suppress("unused")
@ObjCAction
fun keyboardWillHide(arg: NSNotification) {
keyboardOverlapHeightState = 0f
animateKeyboard(arg, false)

if (configuration.onFocusBehavior == OnFocusBehavior.FocusableAboveKeyboard) {
updateViewBounds(offsetY = 0.0)
}
}

private fun animateKeyboard(arg: NSNotification, isShow: Boolean) {
val userInfo = arg.userInfo!!

//return actual keyboard height during animation
fun getCurrentKeyboardHeight(): CGFloat {
val layer = keyboardAnimationView.layer.presentationLayer() ?: return 0.0
return layer.frame.useContents { origin.y }
}

//attach to root view if needed
if (keyboardAnimationView.superview == null) {
this@ComposeWindow.view.addSubview(keyboardAnimationView)
}

//cancel previous animation
keyboardAnimationView.layer.removeAllAnimations()
keyboardAnimationListener?.invalidate()

//synchronize actual keyboard height with keyboardAnimationView without animation
val current = getCurrentKeyboardHeight()
CATransaction.begin()
CATransaction.setValue(true, kCATransactionDisableActions)
keyboardAnimationView.setFrame(CGRectMake(0.0, current, 0.0, 0.0))
CATransaction.commit()

//animation listener
keyboardAnimationListener = CADisplayLink.displayLinkWithTarget(
target = object : NSObject() {
val bottomIndent: CGFloat

init {
val screenHeight = UIScreen.mainScreen.bounds.useContents { size.height }
val composeViewBottomY = UIScreen.mainScreen.coordinateSpace.convertPoint(
point = CGPointMake(0.0, view.frame.useContents { size.height }),
fromCoordinateSpace = view.coordinateSpace
).useContents { y }
bottomIndent = screenHeight - composeViewBottomY
}

@Suppress("unused")
@ObjCAction
fun animationDidUpdate() {
val currentHeight = getCurrentKeyboardHeight()
if (bottomIndent < currentHeight) {
keyboardOverlapHeightState = (currentHeight - bottomIndent).toFloat()
}
}
},
selector = sel_registerName("animationDidUpdate")
).apply {
addToRunLoop(NSRunLoop.mainRunLoop(), NSDefaultRunLoopMode)
}

//start system animation with duration
val duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Double ?: 0.0
val toValue: CGFloat = if (isShow) {
val keyboardInfo = userInfo[UIKeyboardFrameEndUserInfoKey] as NSValue
keyboardInfo.CGRectValue().useContents { size.height }
} else {
0.0
}
UIView.animateWithDuration(
duration = duration,
animations = {
//set final destination for animation
keyboardAnimationView.setFrame(CGRectMake(0.0, toValue, 0.0, 0.0))
},
completion = { isFinished ->
if (isFinished) {
keyboardAnimationListener?.invalidate()
keyboardAnimationListener = null
keyboardAnimationView.removeFromSuperview()
} else {
//animation was canceled by other animation
}
}
)
}

private fun calcFocusedLiftingY(focusedRect: DpRect, keyboardHeight: Double): Double {
val viewHeight = attachedComposeContext?.view?.frame?.useContents {
size.height
Expand Down

0 comments on commit 4739ce3

Please sign in to comment.