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

interpolate from rectangle to shape? #1405

Closed
github-actions bot opened this issue Jul 18, 2023 · 1 comment
Closed

interpolate from rectangle to shape? #1405

github-actions bot opened this issue Jul 18, 2023 · 1 comment
Labels

Comments

@github-actions
Copy link
Contributor

with a higher priority.

https://api.github.com/chrisbanes/tivi/blob/ce1568ef06038915be04606dd892647edec9058f/ui/root/src/androidMain/kotlin/app/tivi/home/GestureNavDecoration.kt#L182


// Copyright 2023, Christopher Banes and the Tivi project contributors
// SPDX-License-Identifier: Apache-2.0

package app.tivi.home

import android.annotation.SuppressLint
import android.os.Build
import android.window.BackEvent
import android.window.OnBackAnimationCallback
import android.window.OnBackInvokedDispatcher
import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.with
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import app.tivi.animations.lerp
import com.slack.circuit.foundation.NavigatorDefaults
import com.slack.circuit.runtime.Navigator
import kotlin.math.absoluteValue

@SuppressLint("NewApi")
@OptIn(ExperimentalAnimationApi::class)
internal actual class GestureNavDecoration actual constructor(
    private val navigator: Navigator,
) : NavDecorationWithPrevious {

    @Composable
    override fun <T> DecoratedContent(
        arg: T,
        previous: T?,
        backStackDepth: Int,
        modifier: Modifier,
        content: @Composable (T) -> Unit,
    ) {
        if (Build.VERSION.SDK_INT < 34) {
            return NavigatorDefaults.DefaultDecoration.DecoratedContent(
                arg = arg,
                backStackDepth = backStackDepth,
                modifier = modifier,
                content = content,
            )
        }

        Box(modifier = modifier) {
            var showPrevious by remember { mutableStateOf(false) }
            var recordPoppedFromGesture by remember { mutableStateOf<T?>(null) }

            if (previous != null) {
                PreviousContent(isVisible = { showPrevious }) {
                    content(previous)
                }
            }

            // Remember the previous stack depth so we know if the navigation is going "back".
            var prevStackDepth by rememberSaveable { mutableStateOf(backStackDepth) }
            SideEffect {
                prevStackDepth = backStackDepth
            }

            val transition = updateTransition(targetState = arg, label = "GestureNavDecoration")

            LaunchedEffect(transition.currentState) {
                // When the current state has changed (i.e. any transition has completed),
                // clear out any transient state
                showPrevious = false
                recordPoppedFromGesture = null
            }

            transition.AnimatedContent(
                modifier = modifier,
                transitionSpec = {
                    // Mirror the forward and backward transitions of activities in Android 33
                    when {
                        // adding to back stack
                        backStackDepth > prevStackDepth -> {
                            (slideInHorizontally(tween(), SlightlyRight) + fadeIn()) with
                                (slideOutHorizontally(tween(), SlightlyLeft) + fadeOut())
                        }

                        // come back from back stack
                        backStackDepth < prevStackDepth -> {
                            if (recordPoppedFromGesture == initialState) {
                                EnterTransition.None with scaleOut(targetScale = 0.8f) + fadeOut()
                            } else {
                                slideInHorizontally(tween(), SlightlyLeft) + fadeIn() with
                                    slideOutHorizontally(tween(), SlightlyRight) + fadeOut()
                            }.apply {
                                targetContentZIndex = -1f
                            }
                        }

                        // Root reset. Crossfade
                        else -> fadeIn() with fadeOut()
                    }
                },
            ) { record ->
                var swipeProgress by remember { mutableStateOf(0f) }

                if (backStackDepth > 1) {
                    BackHandler(
                        onBackProgress = { progress ->
                            showPrevious = progress != 0f
                            swipeProgress = progress
                        },
                        onBackInvoked = {
                            if (swipeProgress != 0f) {
                                // If back has been invoked, and the swipe progress isn't zero,
                                // mark this record as 'popped via gesture' so we can
                                // use a different transition
                                recordPoppedFromGesture = record
                            }
                            navigator.pop()
                        },
                    )
                }

                Box(
                    modifier = Modifier.predictiveBackMotion(
                        shape = MaterialTheme.shapes.extraLarge,
                        progress = { swipeProgress },
                    ),
                ) {
                    content(record)
                }
            }
        }
    }
}

private const val FIVE_PERCENT = 0.05f
private val SlightlyRight = { width: Int -> (width * FIVE_PERCENT).toInt() }
private val SlightlyLeft = { width: Int -> 0 - (width * FIVE_PERCENT).toInt() }

/**
 * Implements most of the treatment specified at
 * https://developer.android.com/design/ui/mobile/guides/patterns/predictive-back#designing-gesture
 */
private fun Modifier.predictiveBackMotion(
    shape: Shape,
    progress: () -> Float,
): Modifier = graphicsLayer {
    val p = progress()
    // If we're at progress 0f, skip setting any parameters
    if (p == 0f) return@graphicsLayer

    translationX = -(8.dp * p).toPx()
    shadowElevation = 6.dp.toPx()

    val scale = lerp(1f, 0.9f, p.absoluteValue)
    scaleX = scale
    scaleY = scale
    transformOrigin = TransformOrigin(
        pivotFractionX = if (p > 0) 1f else 0f,
        pivotFractionY = 0.5f,
    )

    // TODO: interpolate from rectangle to shape?
    this.shape = shape
    clip = true
}

@RequiresApi(34)
@Composable
private fun BackHandler(
    onBackProgress: (Float) -> Unit,
    animatedEnabled: Boolean = true,
    onBackInvoked: () -> Unit,
) {
    val onBackInvokedDispatcher = LocalView.current.findOnBackInvokedDispatcher()
    val lastAnimatedEnabled by rememberUpdatedState(animatedEnabled)
    val lastOnBackProgress by rememberUpdatedState(onBackProgress)
    val lastOnBackInvoked by rememberUpdatedState(onBackInvoked)

    DisposableEffect(onBackInvokedDispatcher) {
        val callback = object : OnBackAnimationCallback {
            override fun onBackStarted(backEvent: BackEvent) {
                if (lastAnimatedEnabled) {
                    lastOnBackProgress(0f)
                }
            }

            override fun onBackProgressed(backEvent: BackEvent) {
                if (lastAnimatedEnabled) {
                    lastOnBackProgress(
                        when (backEvent.swipeEdge) {
                            BackEvent.EDGE_LEFT -> backEvent.progress
                            else -> -backEvent.progress
                        },
                    )
                }
            }

            override fun onBackInvoked() = lastOnBackInvoked()
        }

        onBackInvokedDispatcher?.registerOnBackInvokedCallback(
            // Circuit adds its own BackHandler() at the root, so we need to add a callback
            // with a higher priority.
            OnBackInvokedDispatcher.PRIORITY_DEFAULT + 10,
            callback,
        )

        onDispose {
            onBackInvokedDispatcher?.unregisterOnBackInvokedCallback(callback)
        }
    }
}

@github-actions github-actions bot added the todo label Jul 18, 2023
@github-actions github-actions bot closed this as completed Sep 5, 2023
@github-actions
Copy link
Contributor Author

github-actions bot commented Sep 5, 2023

Closed in 5aa88ac

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

0 participants