Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronize IME insets with iOS keyboard #875

Merged
merged 2 commits into from
Oct 23, 2023
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,15 @@ 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.new()!!.apply {
setFrame(CGRectMake(0.0, 0.0, 0.0, 0.0))
hidden = true
}
terrakok marked this conversation as resolved.
Show resolved Hide resolved
}
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 +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) {
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 +253,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