Animate launcher icon in return to home gesture animation

This commit is contained in:
MM20 2022-10-08 18:41:14 +02:00
parent 709a36fb31
commit a9032735f2
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
7 changed files with 87 additions and 30 deletions

View File

@ -9,8 +9,9 @@ class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
super.onNewIntent(intent) super.onNewIntent(intent)
val navContract = intent?.let { GestureNavContract.fromIntent(it) } val navContract = intent?.let { GestureNavContract.fromIntent(it) }
if (navContract != null) { if (navContract != null) {
homeTransitionManager.resolve(navContract) homeTransitionManager.resolve(navContract, window)
} else { } else {
homeTransitionManager.clear()
onBackPressed() onBackPressed()
} }
} }

View File

@ -1,11 +1,9 @@
package de.mm20.launcher2.ui.launcher package de.mm20.launcher2.ui.launcher
import android.app.WallpaperManager import android.app.WallpaperManager
import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -16,34 +14,40 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.* import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.flowWithLifecycle
import com.android.launcher3.GestureNavContract
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.assistant.AssistantScaffold import de.mm20.launcher2.ui.assistant.AssistantScaffold
import de.mm20.launcher2.ui.base.BaseActivity import de.mm20.launcher2.ui.base.BaseActivity
import de.mm20.launcher2.ui.base.ProvideCurrentTime import de.mm20.launcher2.ui.base.ProvideCurrentTime
import de.mm20.launcher2.ui.base.ProvideSettings import de.mm20.launcher2.ui.base.ProvideSettings
import de.mm20.launcher2.ui.component.NavBarEffects import de.mm20.launcher2.ui.component.NavBarEffects
import de.mm20.launcher2.ui.ktx.animateTo import de.mm20.launcher2.ui.ktx.animateTo
import de.mm20.launcher2.ui.launcher.transitions.HomeTransition
import de.mm20.launcher2.ui.launcher.transitions.HomeTransitionManager import de.mm20.launcher2.ui.launcher.transitions.HomeTransitionManager
import de.mm20.launcher2.ui.launcher.transitions.LocalHomeTransitionManager import de.mm20.launcher2.ui.launcher.transitions.LocalHomeTransitionManager
import de.mm20.launcher2.ui.locals.LocalSnackbarHostState import de.mm20.launcher2.ui.locals.LocalSnackbarHostState
import de.mm20.launcher2.ui.locals.LocalWindowSize import de.mm20.launcher2.ui.locals.LocalWindowSize
import de.mm20.launcher2.ui.theme.LauncherTheme import de.mm20.launcher2.ui.theme.LauncherTheme
import kotlin.math.pow
abstract class SharedLauncherActivity( abstract class SharedLauncherActivity(
@ -86,15 +90,22 @@ abstract class SharedLauncherActivity(
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val enterTransition = remember { mutableStateOf(1f) } val enterTransitionProgress = remember { mutableStateOf(1f) }
var enterTransition by remember {
mutableStateOf<HomeTransition?>(
null
)
}
LaunchedEffect(null) { LaunchedEffect(null) {
homeTransitionManager homeTransitionManager
.currentTransition .currentTransition
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED) .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { .collect {
enterTransition.value = 0f enterTransitionProgress.value = 0f
enterTransition.animateTo(1f) enterTransition = it
enterTransitionProgress.animateTo(1f)
enterTransition = null
} }
} }
@ -138,29 +149,33 @@ abstract class SharedLauncherActivity(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.graphicsLayer { .graphicsLayer {
scaleX = 0.5f + enterTransition.value * 0.5f scaleX =
scaleY = 0.5f + enterTransition.value * 0.5f 0.5f + enterTransitionProgress.value * 0.5f
alpha = enterTransition.value scaleY =
0.5f + enterTransitionProgress.value * 0.5f
alpha = enterTransitionProgress.value
}, },
darkStatusBarIcons = lightStatus, darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav, darkNavBarIcons = lightNav,
) )
} }
Settings.AppearanceSettings.Layout.Pager, Settings.AppearanceSettings.Layout.Pager,
Settings.AppearanceSettings.Layout.PagerReversed -> { Settings.AppearanceSettings.Layout.PagerReversed -> {
PagerScaffold( PagerScaffold(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.graphicsLayer { .graphicsLayer {
scaleX = enterTransition.value scaleX = enterTransitionProgress.value
scaleY = enterTransition.value scaleY = enterTransitionProgress.value
alpha = enterTransition.value alpha = enterTransitionProgress.value
}, },
darkStatusBarIcons = lightStatus, darkStatusBarIcons = lightStatus,
darkNavBarIcons = lightNav, darkNavBarIcons = lightNav,
reverse = layout == Settings.AppearanceSettings.Layout.PagerReversed reverse = layout == Settings.AppearanceSettings.Layout.PagerReversed
) )
} }
else -> {} else -> {}
} }
} }
@ -170,6 +185,25 @@ abstract class SharedLauncherActivity(
.navigationBarsPadding() .navigationBarsPadding()
.imePadding() .imePadding()
) )
enterTransition?.let {
val dX = it.startBounds.center.x - it.targetBounds.center.x
val dY = it.startBounds.center.y - it.targetBounds.center.y
val s = (it.startBounds.minDimension / it.targetBounds.minDimension - 1f) * 0.5f
Box(
modifier = Modifier
.align(Alignment.TopStart)
.graphicsLayer {
val p = (enterTransitionProgress.value).pow(2f)
transformOrigin = TransformOrigin.Center
translationX = it.targetBounds.left + dX * (1 - p)
translationY = it.targetBounds.top + dY * (1 - p)
alpha = enterTransitionProgress.value
scaleX = 1f + s * (1 - p)
scaleY = 1f + s * (1 - p)
}) {
it.icon?.invoke(Offset(dX, dY)) { enterTransitionProgress.value }
}
}
} }
} }
} }

View File

@ -73,7 +73,11 @@ fun GridItem(modifier: Modifier = Modifier, item: Searchable, showLabels: Boolea
bounds.right > 0f && bounds.left < windowSize.width && bounds.right > 0f && bounds.left < windowSize.width &&
bounds.bottom > 0f && bounds.top < windowSize.height bounds.bottom > 0f && bounds.top < windowSize.height
) { ) {
return@HandleHomeTransition HomeTransitionParams(bounds) return@HandleHomeTransition HomeTransitionParams(
bounds
) { _, _ ->
ShapedLauncherIcon(size = LocalGridIconSize.current, icon = { icon })
}
} }
return@HandleHomeTransition null return@HandleHomeTransition null
} }

View File

@ -0,0 +1,11 @@
package de.mm20.launcher2.ui.launcher.transitions
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
data class HomeTransition(
val startBounds: Rect,
val targetBounds: Rect,
val icon: (@Composable (animVector: Offset, progress: () -> Float) -> Unit)? = null
)

View File

@ -1,28 +1,42 @@
package de.mm20.launcher2.ui.launcher.transitions package de.mm20.launcher2.ui.launcher.transitions
import android.view.Window
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.toAndroidRectF import androidx.compose.ui.graphics.toAndroidRectF
import com.android.launcher3.GestureNavContract import com.android.launcher3.GestureNavContract
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
class HomeTransitionManager { class HomeTransitionManager {
val currentTransition = MutableSharedFlow<HomeTransitionParams?>(1) val currentTransition = MutableSharedFlow<HomeTransition?>(1)
private val handlers = mutableSetOf<HomeTransitionHandler>() private val handlers = mutableSetOf<HomeTransitionHandler>()
fun resolve(gestureNavContract: GestureNavContract) { fun resolve(gestureNavContract: GestureNavContract, window: Window) {
for (handler in handlers) { for (handler in handlers) {
val result = handler.handle(gestureNavContract) val result = handler.handle(gestureNavContract)
if (result != null) { if (result != null) {
gestureNavContract.sendEndPosition(result.targetBounds.toAndroidRectF()) val startRect = Rect(Offset(0f, 0f), Size(window.decorView.width.toFloat(), window.decorView.height.toFloat()))
currentTransition.tryEmit(result) val targetBounds = result.targetBounds
gestureNavContract.sendEndPosition(targetBounds.toAndroidRectF())
currentTransition.tryEmit(HomeTransition(
startBounds = startRect,
icon = result.icon,
targetBounds = targetBounds,
))
return return
} }
} }
currentTransition.tryEmit(null) currentTransition.tryEmit(null)
} }
fun clear() {
currentTransition.tryEmit(null)
}
fun registerHandler(handler: HomeTransitionHandler) { fun registerHandler(handler: HomeTransitionHandler) {
handlers.add(handler) handlers.add(handler)
} }

View File

@ -1,8 +0,0 @@
package de.mm20.launcher2.ui.launcher.transitions
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Rect
fun interface HomeTransitionOffer {
fun accept(targetBounds: Rect, icon: @Composable () -> Unit)
}

View File

@ -2,10 +2,11 @@ package de.mm20.launcher2.ui.launcher.transitions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
@Stable @Stable
data class HomeTransitionParams( data class HomeTransitionParams(
val targetBounds: Rect, val targetBounds: Rect,
val icon: (@Composable () -> Unit)? = null val icon: (@Composable (animVector: Offset, progress: () -> Float) -> Unit)? = null
) )