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

Add crossfade animation during orientation change when used within UIKit hierarchy #778

Merged
merged 7 commits into from
Aug 31, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,19 @@ import kotlin.math.roundToInt
import kotlinx.cinterop.CValue
import kotlinx.cinterop.ExportObjCClass
import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.readValue
import kotlinx.cinterop.useContents
import org.jetbrains.skia.Surface
import org.jetbrains.skiko.SkikoKeyboardEvent
import org.jetbrains.skiko.SkikoPointerEvent
import org.jetbrains.skiko.currentNanoTime
import platform.CoreGraphics.CGAffineTransformIdentity
import platform.CoreGraphics.CGAffineTransformInvert
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.UIKit.*
import platform.darwin.NSObject
Expand Down Expand Up @@ -94,6 +99,41 @@ private class AttachedComposeContext(
val scene: ComposeScene,
val view: SkikoUIView,
) {
private var constraints: List<NSLayoutConstraint> = listOf()

fun setConstraintsToCenterInView(parentView: UIView, size: CValue<CGSize>) {
size.useContents {
setConstraints(
newConstraints = listOf(
view.centerXAnchor.constraintEqualToAnchor(parentView.centerXAnchor),
view.centerYAnchor.constraintEqualToAnchor(parentView.centerYAnchor),
view.widthAnchor.constraintEqualToConstant(width),
view.heightAnchor.constraintEqualToConstant(height)
)
)
}
}

fun setConstraintsToFillView(parentView: UIView) {
setConstraints(
newConstraints = listOf(
view.leftAnchor.constraintEqualToAnchor(parentView.leftAnchor),
view.rightAnchor.constraintEqualToAnchor(parentView.rightAnchor),
view.topAnchor.constraintEqualToAnchor(parentView.topAnchor),
view.bottomAnchor.constraintEqualToAnchor(parentView.bottomAnchor)
)
)
}

private fun setConstraints(newConstraints: List<NSLayoutConstraint>) {
if (constraints.isNotEmpty()) {
NSLayoutConstraint.deactivateConstraints(constraints)
}

constraints = newConstraints
NSLayoutConstraint.activateConstraints(newConstraints)
}

elijah-semyonov marked this conversation as resolved.
Show resolved Hide resolved
fun dispose() {
scene.close()
view.dispose()
Expand Down Expand Up @@ -164,7 +204,10 @@ internal actual class ComposeWindow : UIViewController {
}

private val density: Density
get() = Density(attachedComposeContext?.view?.contentScaleFactor?.toFloat() ?: 1f, fontScale)
get() = Density(
attachedComposeContext?.view?.contentScaleFactor?.toFloat() ?: 1f,
fontScale
)

private lateinit var content: @Composable () -> Unit

Expand Down Expand Up @@ -311,6 +354,60 @@ internal actual class ComposeWindow : UIViewController {
context.view.needRedraw()
}

override fun viewWillTransitionToSize(
size: CValue<CGSize>,
withTransitionCoordinator: UIViewControllerTransitionCoordinatorProtocol
) {
super.viewWillTransitionToSize(size, withTransitionCoordinator)

val attachedComposeContext = attachedComposeContext ?: return

// Happens during orientation change from LandscapeLeft to LandscapeRight, for example
val isSameSizeTransition = view.frame.useContents {
CGSizeEqualToSize(size, this.size.readValue())
}
if (isSameSizeTransition) {
return
}

val startSnapshotView =
attachedComposeContext.view.snapshotViewAfterScreenUpdates(false) ?: return

startSnapshotView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(startSnapshotView)
size.useContents {
NSLayoutConstraint.activateConstraints(
listOf(
startSnapshotView.widthAnchor.constraintEqualToConstant(height),
startSnapshotView.heightAnchor.constraintEqualToConstant(width),
startSnapshotView.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor),
startSnapshotView.centerYAnchor.constraintEqualToAnchor(view.centerYAnchor)
)
)
}

attachedComposeContext.view.isForcedToPresentWithTransactionEveryFrame = true

attachedComposeContext.setConstraintsToCenterInView(view, size)
attachedComposeContext.view.transform = withTransitionCoordinator.targetTransform

view.layoutIfNeeded()

withTransitionCoordinator.animateAlongsideTransition(
animation = {
startSnapshotView.alpha = 0.0
startSnapshotView.transform =
CGAffineTransformInvert(withTransitionCoordinator.targetTransform)
attachedComposeContext.view.transform = CGAffineTransformIdentity.readValue()
},
completion = {
startSnapshotView.removeFromSuperview()
attachedComposeContext.setConstraintsToFillView(view)
attachedComposeContext.view.isForcedToPresentWithTransactionEveryFrame = false
}
)
}

override fun viewWillAppear(animated: Boolean) {
super.viewWillAppear(animated)

Expand Down Expand Up @@ -386,15 +483,6 @@ internal actual class ComposeWindow : UIViewController {
skikoUIView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(skikoUIView)

NSLayoutConstraint.activateConstraints(
listOf(
skikoUIView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor),
skikoUIView.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor),
skikoUIView.topAnchor.constraintEqualToAnchor(view.topAnchor),
skikoUIView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor)
)
)

val inputServices = UIKitTextInputService(
showSoftwareKeyboard = {
skikoUIView.showScreenKeyboard()
Expand Down Expand Up @@ -485,7 +573,10 @@ internal actual class ComposeWindow : UIViewController {
override fun pointInside(point: CValue<CGPoint>, event: UIEvent?): Boolean =
point.useContents {
val hitsInteropView = attachedComposeContext?.scene?.mainOwner?.hitInteropView(
pointerPosition = Offset((x * density.density).toFloat(), (y * density.density).toFloat()),
pointerPosition = Offset(
(x * density.density).toFloat(),
(y * density.density).toFloat()
),
isTouchEvent = true,
) ?: false

Expand Down Expand Up @@ -544,6 +635,7 @@ internal actual class ComposeWindow : UIViewController {

attachedComposeContext =
AttachedComposeContext(scene, skikoUIView).also {
it.setConstraintsToFillView(view)
updateLayout(it)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ internal class MetalRedrawer(
// Semaphore for preventing command buffers count more than swapchain size to be scheduled/executed at the same time
private val inflightSemaphore = dispatch_semaphore_create(metalLayer.maximumDrawableCount.toLong())

var isForcedToPresentWithTransactionEveryFrame = false

var maximumFramesPerSecond: NSInteger
get() = caDisplayLink?.preferredFramesPerSecond ?: 0
set(value) {
Expand Down Expand Up @@ -287,12 +289,14 @@ internal class MetalRedrawer(
surface.flushAndSubmit()

val caTransactionCommands = retrieveCATransactionCommands()
metalLayer.presentsWithTransaction = caTransactionCommands.isNotEmpty()
val presentsWithTransaction = isForcedToPresentWithTransactionEveryFrame || caTransactionCommands.isNotEmpty()

metalLayer.presentsWithTransaction = presentsWithTransaction

val commandBuffer = queue.commandBuffer()!!
commandBuffer.label = "Present"

if (caTransactionCommands.isEmpty()) {
if (!presentsWithTransaction) {
// If there are no pending changes in UIKit interop, present the drawable ASAP
commandBuffer.presentDrawable(metalDrawable)
}
Expand All @@ -303,7 +307,7 @@ internal class MetalRedrawer(
}
commandBuffer.commit()

if (caTransactionCommands.isNotEmpty()) {
if (presentsWithTransaction) {
// If there are pending changes in UIKit interop, [waitUntilScheduled](https://developer.apple.com/documentation/metal/mtlcommandbuffer/1443036-waituntilscheduled) is called
// to ensure that transaction is available
commandBuffer.waitUntilScheduled()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import androidx.compose.ui.platform.IOSSkikoInput
import androidx.compose.ui.platform.SkikoUITextInputTraits
import androidx.compose.ui.platform.TextActions
import kotlinx.cinterop.*
import org.jetbrains.skia.Point
import org.jetbrains.skia.Rect
import platform.CoreGraphics.*
import platform.Foundation.*
Expand Down Expand Up @@ -122,6 +121,8 @@ internal class SkikoUIView : UIView, UIKeyInputProtocol, UITextInputProtocol {

fun needRedraw() = _redrawer.needRedraw()

var isForcedToPresentWithTransactionEveryFrame by _redrawer::isForcedToPresentWithTransactionEveryFrame

/**
* Show copy/paste text menu
* @param targetRect - rectangle of selected text area
Expand Down