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 21, 2023
1 parent 4013a7d commit 481b953
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,9 @@ 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 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 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 @@ -132,8 +132,9 @@ 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 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 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,92 @@
package com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.predictiveback

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
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 com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.essenty.backhandler.BackEvent
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

@ExperimentalDecomposeApi
internal class MaterialPredictiveBackAnimatable(
private val initialEvent: BackEvent,
private val shape: (progress: Float, edge: BackEvent.SwipeEdge) -> Shape,
) : 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() = Modifier.graphicsLayer { setupExitGraphicLayer() }

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

private fun GraphicsLayerScope.setupExitGraphicLayer() {
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 = shape(progress, edge)
clip = true
}

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 {
launch { progressAnimatable.animateTo(progress + (1F - progress) * velocityFactor) }
launch { finishProgressAnimatable.animateTo(targetValue = 0F, animationSpec = tween()) }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
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.graphics.Shape
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.essenty.backhandler.BackEvent
Expand Down Expand Up @@ -61,33 +56,29 @@ 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))
}
/**
* 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 a clipping shape of the child being removed (the currently active child),
* default is [RoundedCornerShape] that gradually increases following the gesture progress.
*/
@ExperimentalDecomposeApi
fun materialPredictiveBackAnimatable(
initialBackEvent: BackEvent,
shape: (progress: Float, edge: BackEvent.SwipeEdge) -> Shape = { progress, _ -> RoundedCornerShape(size = 16.dp * progress) },
): PredictiveBackAnimatable =
MaterialPredictiveBackAnimatable(
initialEvent = initialBackEvent,
shape = shape,
)
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
Expand Up @@ -120,8 +120,9 @@ public abstract interface class com/arkivanov/decompose/extensions/compose/jetpa
}

public final class com/arkivanov/decompose/extensions/compose/jetpack/stack/animation/predictiveback/PredictiveBackAnimatableKt {
public static final fun materialPredictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetpack/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/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable;
public static final fun predictiveBackAnimatable (Lcom/arkivanov/essenty/backhandler/BackEvent;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/arkivanov/decompose/extensions/compose/jetpack/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/jetpack/stack/animation/predictiveback/PredictiveBackAnimatable;
}

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

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
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 com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.essenty.backhandler.BackEvent
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

@ExperimentalDecomposeApi
internal class MaterialPredictiveBackAnimatable(
private val initialEvent: BackEvent,
private val shape: (progress: Float, edge: BackEvent.SwipeEdge) -> Shape,
) : 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() = Modifier.graphicsLayer { setupExitGraphicLayer() }

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

private fun GraphicsLayerScope.setupExitGraphicLayer() {
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 = shape(progress, edge)
clip = true
}

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 {
launch { progressAnimatable.animateTo(progress + (1F - progress) * velocityFactor) }
launch { finishProgressAnimatable.animateTo(targetValue = 0F, animationSpec = tween()) }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package com.arkivanov.decompose.extensions.compose.jetpack.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.graphics.Shape
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.essenty.backhandler.BackEvent
Expand Down Expand Up @@ -61,33 +56,29 @@ 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))
}
/**
* 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 a clipping shape of the child being removed (the currently active child),
* default is [RoundedCornerShape] that gradually increases following the gesture progress.
*/
@ExperimentalDecomposeApi
fun materialPredictiveBackAnimatable(
initialBackEvent: BackEvent,
shape: (progress: Float, edge: BackEvent.SwipeEdge) -> Shape = { progress, _ -> RoundedCornerShape(size = 16.dp * progress) },
): PredictiveBackAnimatable =
MaterialPredictiveBackAnimatable(
initialEvent = initialBackEvent,
shape = shape,
)
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

0 comments on commit 481b953

Please sign in to comment.