Skip to content

Commit

Permalink
Synchronize IME insets with iOS keyboard (#875)
Browse files Browse the repository at this point in the history
Proper support of the keyboard animation

---------

Co-authored-by: dima.avdeev <dima.avdeev@jetbrains.com>
  • Loading branch information
terrakok and dima-avdeev-jb committed Oct 23, 2023
1 parent dd86b0b commit 9eef63d
Showing 1 changed file with 99 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,24 @@ import org.jetbrains.skia.Canvas
import org.jetbrains.skiko.OS
import org.jetbrains.skiko.OSVersion
import org.jetbrains.skiko.SkikoKeyboardEvent
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 @@ -156,6 +161,14 @@ internal actual class ComposeWindow : UIViewController {
private var safeArea by mutableStateOf(PlatformInsets())
private var layoutMargins by mutableStateOf(PlatformInsets())

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

/*
* Initial value is arbitrarily 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 @@ -219,22 +232,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) {
keyboardOverlapHeight = (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 @@ -249,12 +252,92 @@ internal actual class ComposeWindow : UIViewController {
@Suppress("unused")
@ObjCAction
fun keyboardWillHide(arg: NSNotification) {
keyboardOverlapHeight = 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) {
keyboardOverlapHeight = (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 9eef63d

Please sign in to comment.