Skip to content

Commit

Permalink
Added materialPredictiveBackAnimatable as default
Browse files Browse the repository at this point in the history
  • Loading branch information
arkivanov committed Nov 24, 2023
1 parent 4013a7d commit 91aeb5e
Show file tree
Hide file tree
Showing 13 changed files with 572 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/an
public static synthetic fun stackAnimator$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/MaterialPredictiveBackAnimatableKt {
public static final fun materialPredictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable;
public static synthetic fun materialPredictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable;
}

public abstract interface class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable {
public abstract fun animate (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun finish (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand All @@ -121,7 +126,6 @@ public abstract interface class com/arkivanov/decompose/extensions/compose/jetbr

public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatableKt {
public static final fun predictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable;
public static synthetic fun predictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimationKt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/an
public static synthetic fun stackAnimator$default (Landroidx/compose/animation/core/FiniteAnimationSpec;Lkotlin/jvm/functions/Function5;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/StackAnimator;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/MaterialPredictiveBackAnimatableKt {
public static final fun materialPredictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable;
public static synthetic fun materialPredictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable;
}

public abstract interface class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable {
public abstract fun animate (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun finish (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
Expand All @@ -133,7 +138,6 @@ public abstract interface class com/arkivanov/decompose/extensions/compose/jetbr

public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatableKt {
public static final fun predictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable;
public static synthetic fun predictiveBackAnimatable$default (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimatable;
}

public final class com/arkivanov/decompose/extensions/compose/jetbrains/stack/animation/predictiveback/PredictiveBackAnimationKt {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback

import android.content.Context
import android.os.Build
import android.view.RoundedCorner
import android.view.View
import android.view.WindowManager
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.core.content.ContextCompat.getSystemService

internal actual fun Modifier.withLayoutCorners(block: Modifier.(LayoutCorners) -> Modifier): Modifier =
composed {
val context = LocalContext.current
val density = LocalDensity.current
val screenInfo = remember(context) { context.getScreenInfo(density) }

if (screenInfo != null) {
val rootView = LocalView.current
val layoutDirection = LocalLayoutDirection.current
var positionOnScreen by remember { mutableStateOf<Rect?>(null) }
val corners = getLayoutCorners(screenInfo, positionOnScreen, layoutDirection)

onGloballyPositioned { coords ->
positionOnScreen = getBoundsOnScreen(rootView = rootView, boundsInRoot = coords.boundsInRoot())
}.block(corners)
} else {
block(LayoutCorners())
}
}

private fun Context.getScreenInfo(density: Density): ScreenInfo? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return null
}

val windowMetrics = requireNotNull(getSystemService(this, WindowManager::class.java)).maximumWindowMetrics
val insets = windowMetrics.windowInsets

return with(density) {
ScreenInfo(
cornerRadii = CornerRadii(
topLeft = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)?.radius?.toDp(),
topRight = insets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT)?.radius?.toDp(),
bottomRight = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT)?.radius?.toDp(),
bottomLeft = insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT)?.radius?.toDp(),
),
width = windowMetrics.bounds.width(),
height = windowMetrics.bounds.height(),
)
}
}

private fun getLayoutCorners(
screenInfo: ScreenInfo,
positionOnScreen: Rect?,
layoutDirection: LayoutDirection,
): LayoutCorners {
if (positionOnScreen == null) {
return LayoutCorners()
}

val (cornerRadii, screenWidth, screenHeight) = screenInfo
val (left, top, right, bottom) = positionOnScreen

val topLeft = getLayoutCorner(radius = cornerRadii.topLeft, isFixed = (left <= 0) && (top <= 0))
val topRight = getLayoutCorner(radius = cornerRadii.topRight, isFixed = (right >= screenWidth) && (top <= 0))
val bottomRight = getLayoutCorner(radius = cornerRadii.bottomRight, isFixed = (right >= screenWidth) && (bottom >= screenHeight))
val bottomLeft = getLayoutCorner(radius = cornerRadii.bottomLeft, isFixed = (left <= 0) && (bottom >= screenHeight))

return when (layoutDirection) {
LayoutDirection.Ltr -> LayoutCorners(topStart = topLeft, topEnd = topRight, bottomEnd = bottomRight, bottomStart = bottomLeft)
LayoutDirection.Rtl -> LayoutCorners(topStart = topRight, topEnd = topLeft, bottomEnd = bottomLeft, bottomStart = bottomRight)
}
}

private fun getLayoutCorner(radius: Dp?, isFixed: Boolean): LayoutCorner =
if (radius == null) {
LayoutCorner()
} else {
LayoutCorner(radius = radius, isFixed = isFixed)
}

private fun getBoundsOnScreen(rootView: View, boundsInRoot: Rect): Rect {
val rootViewLeftTopOnScreen = IntArray(2)
rootView.getLocationOnScreen(rootViewLeftTopOnScreen)
val (rootViewLeftOnScreen, rootViewTopOnScreen) = rootViewLeftTopOnScreen

return Rect(
left = rootViewLeftOnScreen + boundsInRoot.left,
top = rootViewTopOnScreen + boundsInRoot.top,
right = rootViewLeftOnScreen + boundsInRoot.right,
bottom = rootViewTopOnScreen + boundsInRoot.bottom,
)
}

private data class ScreenInfo(
val cornerRadii: CornerRadii,
val width: Int,
val height: Int,
)

private data class CornerRadii(
val topLeft: Dp? = null,
val topRight: Dp? = null,
val bottomRight: Dp? = null,
val bottomLeft: Dp? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback

import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

internal expect fun Modifier.withLayoutCorners(block: Modifier.(LayoutCorners) -> Modifier): Modifier

internal data class LayoutCorners(
val topStart: LayoutCorner = LayoutCorner(),
val topEnd: LayoutCorner = LayoutCorner(),
val bottomEnd: LayoutCorner = LayoutCorner(),
val bottomStart: LayoutCorner = LayoutCorner(),
)

internal data class LayoutCorner(
val radius: Dp = 16.dp,
val isFixed: Boolean = true,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback

import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.GraphicsLayerScope
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.essenty.backhandler.BackEvent
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch

/**
* Creates an implementation of [PredictiveBackAnimatable] that resembles the
* [predictive back design for Android](https://developer.android.com/design/ui/mobile/guides/patterns/predictive-back).
*
* @param initialBackEvent an initial [BackEvent] of the predictive back gesture.
* @param shape an optional clipping shape of the child being removed (the currently active child).
* If not supplied then a [RoundedCornerShape][androidx.compose.foundation.shape.RoundedCornerShape] will be applied.
*/
@ExperimentalDecomposeApi
fun materialPredictiveBackAnimatable(
initialBackEvent: BackEvent,
shape: ((progress: Float, edge: BackEvent.SwipeEdge) -> Shape)? = null,
): PredictiveBackAnimatable =
MaterialPredictiveBackAnimatable(
initialEvent = initialBackEvent,
shape = shape,
)

@ExperimentalDecomposeApi
private class MaterialPredictiveBackAnimatable(
private val initialEvent: BackEvent,
private val shape: ((progress: Float, edge: BackEvent.SwipeEdge) -> Shape)? = null,
) : PredictiveBackAnimatable {

private val finishProgressAnimatable = Animatable(initialValue = 1F)
private val finishProgress by derivedStateOf { finishProgressAnimatable.value }
private val progressAnimatable = Animatable(initialValue = initialEvent.progress)
private val progress by derivedStateOf { progressAnimatable.value }
private var edge by mutableStateOf(initialEvent.swipeEdge)
private var touchY by mutableFloatStateOf(initialEvent.touchY)

override val exitModifier: Modifier
get() =
if (shape == null) {
Modifier.withLayoutCorners { corners ->
graphicsLayer { setupExitGraphicLayer(corners.toShape()) }
}
} else {
Modifier.graphicsLayer {
setupExitGraphicLayer(this@MaterialPredictiveBackAnimatable.shape.invoke(progress, edge))
}
}

override val enterModifier: Modifier
get() =
Modifier.drawWithContent {
drawContent()
drawRect(color = Color.Black.copy(alpha = finishProgress * 0.25F))
}

private fun GraphicsLayerScope.setupExitGraphicLayer(layoutShape: Shape) {
val pivotFractionX =
when (edge) {
BackEvent.SwipeEdge.LEFT -> 1F
BackEvent.SwipeEdge.RIGHT -> 0F
BackEvent.SwipeEdge.UNKNOWN -> 0.5F
}

transformOrigin = TransformOrigin(pivotFractionX = pivotFractionX, pivotFractionY = 0.5F)

val scale = 1F - progress / 10F
scaleX = scale
scaleY = scale

val translationXLimit =
when (edge) {
BackEvent.SwipeEdge.LEFT -> -8.dp.toPx()
BackEvent.SwipeEdge.RIGHT -> 8.dp.toPx()
BackEvent.SwipeEdge.UNKNOWN -> 0F
}

translationX = translationXLimit * progress

val translationYLimit = size.height / 20F - 8.dp.toPx()
val translationYFactor = ((touchY - initialEvent.touchY) / size.height) * (progress * 3F).coerceAtMost(1f)
translationY = translationYLimit * translationYFactor

alpha = finishProgress
shape = layoutShape
clip = true
}

private fun LayoutCorners.toShape(): RoundedCornerShape =
RoundedCornerShape(
topStart = topStart.getProgressRadius(),
topEnd = topEnd.getProgressRadius(),
bottomEnd = bottomEnd.getProgressRadius(),
bottomStart = bottomStart.getProgressRadius(),
)

private fun LayoutCorner.getProgressRadius(): Dp =
if (isFixed) radius else radius * progress

override suspend fun animate(event: BackEvent) {
edge = event.swipeEdge
touchY = event.touchY
progressAnimatable.animateTo(event.progress)
}

override suspend fun finish() {
val velocityFactor = progressAnimatable.velocity.coerceAtMost(1F) / 1F
val progress = progressAnimatable.value
coroutineScope {
joinAll(
launch { progressAnimatable.animateTo(progress + (1F - progress) * velocityFactor) },
launch { finishProgressAnimatable.animateTo(targetValue = 0F) },
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback

import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.graphics.Shape
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.essenty.backhandler.BackEvent

Expand Down Expand Up @@ -61,33 +54,11 @@ interface PredictiveBackAnimatable {
@ExperimentalDecomposeApi
fun predictiveBackAnimatable(
initialBackEvent: BackEvent,
exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, edge ->
Modifier.exitModifier(progress = progress, edge = edge)
},
enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier = { progress, _ ->
Modifier.enterModifier(progress = progress)
},
exitModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier,
enterModifier: (progress: Float, edge: BackEvent.SwipeEdge) -> Modifier,
): PredictiveBackAnimatable =
DefaultPredictiveBackAnimatable(
initialBackEvent = initialBackEvent,
getExitModifier = exitModifier,
getEnterModifier = enterModifier,
)

private fun Modifier.exitModifier(progress: Float, edge: BackEvent.SwipeEdge): Modifier =
scale(1F - progress * 0.25F)
.absoluteOffset(
x = when (edge) {
BackEvent.SwipeEdge.LEFT -> 32.dp * progress
BackEvent.SwipeEdge.RIGHT -> (-32).dp * progress
BackEvent.SwipeEdge.UNKNOWN -> 0.dp
},
)
.alpha(((1F - progress) * 2F).coerceAtMost(1F))
.clip(RoundedCornerShape(size = 64.dp * progress))

private fun Modifier.enterModifier(progress: Float): Modifier =
drawWithContent {
drawContent()
drawRect(color = Color(red = 0F, green = 0F, blue = 0F, alpha = (1F - progress) / 4F))
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fun <C : Any, T : Any> predictiveBackAnimation(
exitChild: Child.Created<C, T>,
enterChild: Child.Created<C, T>,
) -> PredictiveBackAnimatable = { initialBackEvent, _, _ ->
predictiveBackAnimatable(initialBackEvent = initialBackEvent)
materialPredictiveBackAnimatable(initialBackEvent = initialBackEvent)
},
onBack: () -> Unit,
): StackAnimation<C, T> =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback

import androidx.compose.ui.Modifier

internal actual fun Modifier.withLayoutCorners(block: Modifier.(LayoutCorners) -> Modifier): Modifier =
block(LayoutCorners())
Loading

0 comments on commit 91aeb5e

Please sign in to comment.