Skip to content

Commit

Permalink
Animate compose content size transitions (#1691)
Browse files Browse the repository at this point in the history
Animate the size transition of Compose scenes and platform windows when
the size of the ComposeUIViewController changes.
Example:
https://github.com/user-attachments/assets/0054014c-fc8e-419d-8ffb-eaf6497dfb5e

Fixes
https://youtrack.jetbrains.com/issue/CMP-1491/Fix-interop-views-animation-while-rotating-screen

## Testing
### Features - iOS
- Animate the size transition of Compose content when the screen is
rotated or other ComposeUIViewController size changes
  • Loading branch information
ASalavei authored Nov 29, 2024
1 parent 0e74f3d commit b163bf7
Show file tree
Hide file tree
Showing 15 changed files with 424 additions and 320 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.ui.animation

import androidx.compose.runtime.withFrameNanos
import kotlin.math.min
import kotlin.time.Duration
import kotlin.time.Duration.Companion.nanoseconds

internal fun easeInOutTimingFunction(progress: Float): Float = if (progress < 0.5f) {
2f * progress * progress
} else {
(-2f * progress * progress) + (4f * progress) - 1f
}

internal suspend fun withAnimationProgress(
duration: Duration,
timingFunction: (Float) -> Float = ::easeInOutTimingFunction,
update: (Float) -> Unit
) {
update(0f)

var firstFrameTime = 0L
var progressDuration = Duration.ZERO
while (progressDuration < duration) {
withFrameNanos { frameTime ->
if (firstFrameTime == 0L) {
firstFrameTime = frameTime
}
progressDuration = (frameTime - firstFrameTime).nanoseconds
val progress = timingFunction(
min(1.0, progressDuration / duration).toFloat()
)
update(progress)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.animation.withAnimationProgress
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.platform.LocalDensity
Expand All @@ -36,7 +36,6 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.time.Duration
import kotlin.time.Duration.Companion.nanoseconds

@Composable
internal fun OffsetToFocusedRect(
Expand Down Expand Up @@ -156,28 +155,3 @@ private fun directionalFocusOffset(
min(0f, max(hiddenFromPart, -hiddenToPart)).roundToInt()
}
}

private suspend fun withAnimationProgress(duration: Duration, update: (Float) -> Unit) {
fun easeInOutProgress(progress: Float) = if (progress < 0.5) {
2 * progress * progress
} else {
(-2 * progress * progress) + (4 * progress) - 1
}

update(0f)

var firstFrameTime = 0L
var progressDuration = Duration.ZERO
while (progressDuration < duration) {
withFrameNanos { frameTime ->
if (firstFrameTime == 0L) {
firstFrameTime = frameTime
}
progressDuration = (frameTime - firstFrameTime).nanoseconds
val progress = easeInOutProgress(
min(1.0, progressDuration / duration).toFloat()
)
update(progress)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.runtime.Stable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.lerp

/**
* This class represents platform insets.
Expand Down Expand Up @@ -81,6 +82,14 @@ internal fun PlatformInsets.exclude(insets: PlatformInsets) = PlatformInsets(
bottom = (bottom - insets.bottom).coerceAtLeast(0.dp)
)

internal fun lerp(start: PlatformInsets, stop: PlatformInsets, fraction: Float) =
PlatformInsets(
left = lerp(start.left, stop.left, fraction),
right = lerp(start.right, stop.right, fraction),
top = lerp(start.top, stop.top, fraction),
bottom = lerp(start.bottom, stop.bottom, fraction)
)

internal interface InsetsConfig {

// TODO: Add more granular control. Look at Android's [WindowInsetsCompat]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,34 @@

package androidx.compose.ui.platform

import androidx.compose.ui.animation.withAnimationProgress
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.lerp
import androidx.compose.ui.uikit.density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.asCGPoint
import androidx.compose.ui.unit.asCGRect
import androidx.compose.ui.unit.asDpOffset
import androidx.compose.ui.unit.asDpRect
import androidx.compose.ui.unit.asDpSize
import androidx.compose.ui.unit.roundToIntSize
import androidx.compose.ui.unit.toDpOffset
import androidx.compose.ui.unit.toDpRect
import androidx.compose.ui.unit.toOffset
import androidx.compose.ui.unit.toRect
import kotlin.math.roundToInt
import androidx.compose.ui.unit.toSize
import kotlin.time.Duration
import kotlinx.cinterop.useContents
import platform.UIKit.UIView

private const val LayerFrameKeyPath = "layer.frame"

/**
* Tracking a state of window.
*/
internal class PlatformWindowContext {
private val _windowInfo = WindowInfoImpl()
private val _windowInfo = WindowInfoImpl().apply {
isWindowFocused = true
}

val windowInfo: WindowInfo get() = _windowInfo

Expand All @@ -47,26 +52,47 @@ internal class PlatformWindowContext {
*/
private var windowContainer: UIView? = null

var isWindowFocused by _windowInfo::isWindowFocused

fun setWindowContainer(windowContainer: UIView) {
this.windowContainer = windowContainer

updateWindowContainerSize()
}

fun updateWindowContainerSize() {
val windowContainer = windowContainer ?: return

val scale = windowContainer.density.density
val size = windowContainer.frame.useContents {
IntSize(
width = (size.width * scale).roundToInt(),
height = (size.height * scale).roundToInt()
)
private var isAnimating = false
fun prepareAndGetSizeTransitionAnimation(): suspend (Duration) -> Unit {
isAnimating = true
val initialSize = _windowInfo.containerSize.toSize()

return { duration ->
try {
if (initialSize != currentWindowContainerSize) {
withAnimationProgress(duration) { progress ->
val size = currentWindowContainerSize ?: initialSize
_windowInfo.containerSize =
lerp(initialSize, size, progress).roundToIntSize()
}
}
} finally {
isAnimating = false
updateWindowContainerSize()
}
}
}

_windowInfo.containerSize = size
fun updateWindowContainerSize() {
if (isAnimating) return

_windowInfo.containerSize = currentWindowContainerSize?.roundToIntSize() ?: return
}

private val currentWindowContainerSize: Size? get() {
val windowContainer = windowContainer ?: return null

return windowContainer.bounds.useContents {
with(windowContainer.density) {
size.asDpSize().toSize()
}
}
}

fun convertLocalToWindowPosition(container: UIView, localPosition: Offset): Offset {
Expand Down
Loading

0 comments on commit b163bf7

Please sign in to comment.