(fix) home transition to target that is no longer in view

This commit is contained in:
MM20 2025-06-30 20:11:03 +02:00
parent 8f4766d28a
commit 46d7293175
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
6 changed files with 129 additions and 72 deletions

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.launcher
import android.content.Intent import android.content.Intent
import com.android.launcher3.GestureNavContract import com.android.launcher3.GestureNavContract
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage
class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) { class LauncherActivity: SharedLauncherActivity(LauncherActivityMode.Launcher) {
@ -9,7 +10,12 @@ 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) {
enterHomeTransitionManager.resolve(navContract, window) val page = if (System.currentTimeMillis() - pauseTime > 5000L || pauseOnHome) {
ScaffoldPage.Home
} else {
ScaffoldPage.Secondary
}
enterHomeTransitionManager.resolve(navContract, window, page)
} }
} }

View File

@ -432,6 +432,12 @@ abstract class SharedLauncherActivity(
} }
} }
var pauseTime = 0L
/**
* True if the scaffold was on home screen when the activity was paused.
*/
var pauseOnHome = false
var isNewIntent = false var isNewIntent = false
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)

View File

@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.add
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.ime
@ -39,7 +38,6 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
@ -49,6 +47,7 @@ import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@ -245,14 +244,16 @@ internal class LauncherScaffoldState(
initialIsLocked: Boolean = false, initialIsLocked: Boolean = false,
initialIsSearchBarHidden: Boolean = false, initialIsSearchBarHidden: Boolean = false,
) { ) {
var currentOffset by mutableStateOf(when { var currentOffset by mutableStateOf(
initialGesture == null || initialGesture.orientation == null || config[initialGesture]?.animation == ScaffoldAnimation.Rubberband -> Offset.Zero when {
initialGesture == Gesture.SwipeRight -> Offset(-size.width, 0f) initialGesture == null || initialGesture.orientation == null || config[initialGesture]?.animation == ScaffoldAnimation.Rubberband -> Offset.Zero
initialGesture == Gesture.SwipeLeft -> Offset(size.width, 0f) initialGesture == Gesture.SwipeRight -> Offset(-size.width, 0f)
initialGesture == Gesture.SwipeUp -> Offset(0f, -size.height) initialGesture == Gesture.SwipeLeft -> Offset(size.width, 0f)
initialGesture == Gesture.SwipeDown -> Offset(0f, size.height) initialGesture == Gesture.SwipeUp -> Offset(0f, -size.height)
else -> Offset.Zero initialGesture == Gesture.SwipeDown -> Offset(0f, size.height)
}) else -> Offset.Zero
}
)
private set private set
var currentZOffset by mutableFloatStateOf( var currentZOffset by mutableFloatStateOf(
if (initialGesture != null && initialGesture.orientation == null) 1f else 0f if (initialGesture != null && initialGesture.orientation == null) 1f else 0f
@ -1176,10 +1177,10 @@ internal fun LauncherScaffold(
} }
LaunchedEffect(state.isAtTop, state.isAtBottom) { LaunchedEffect(state.isAtTop, state.isAtBottom) {
if (state.currentProgress > 0f && state.currentProgress < 1f){ if (state.currentProgress > 0f && state.currentProgress < 1f) {
return@LaunchedEffect return@LaunchedEffect
} }
when(state.currentComponent?.reverseScrolling) { when (state.currentComponent?.reverseScrolling) {
true -> if (state.isAtBottom) state.resetSearchBarOffset() true -> if (state.isAtBottom) state.resetSearchBarOffset()
false -> if (state.isAtTop) state.resetSearchBarOffset() false -> if (state.isAtTop) state.resetSearchBarOffset()
else -> {} else -> {}
@ -1211,10 +1212,11 @@ internal fun LauncherScaffold(
config.homeComponent.onPreActivate(state) config.homeComponent.onPreActivate(state)
config.homeComponent.onActivate(state) config.homeComponent.onActivate(state)
var pauseTime = 0L val activity = (activity as? SharedLauncherActivity) ?: return@LaunchedEffect
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
try { try {
if (pauseTime > 0L && System.currentTimeMillis() - pauseTime < 50L && (activity as? SharedLauncherActivity)?.isNewIntent == true) { if (activity.pauseTime > 0L && System.currentTimeMillis() - activity.pauseTime < 50L && activity.isNewIntent) {
if (!state.isLocked) { if (!state.isLocked) {
if (state.currentProgress > 0f) { if (state.currentProgress > 0f) {
state.onPredictiveBackEnd() state.onPredictiveBackEnd()
@ -1224,7 +1226,7 @@ internal fun LauncherScaffold(
} else { } else {
activity.onBackPressedDispatcher.onBackPressed() activity.onBackPressedDispatcher.onBackPressed()
} }
} else if (pauseTime > 0L && System.currentTimeMillis() - pauseTime > 5000L) { } else if (activity.pauseTime > 0L && System.currentTimeMillis() - activity.pauseTime > 5000L) {
if (!state.isLocked) { if (!state.isLocked) {
state.reset() state.reset()
searchVM.reset() searchVM.reset()
@ -1232,7 +1234,8 @@ internal fun LauncherScaffold(
} }
awaitCancellation() awaitCancellation()
} finally { } finally {
pauseTime = System.currentTimeMillis() activity.pauseTime = System.currentTimeMillis()
activity.pauseOnHome = !state.isSettledOnSecondaryPage
} }
} }
} }
@ -1260,8 +1263,10 @@ internal fun LauncherScaffold(
if (config.wallpaperBlurRadius > 0.dp) { if (config.wallpaperBlurRadius > 0.dp) {
val wallpaperBlur by animateIntAsState( val wallpaperBlur by animateIntAsState(
if (state.currentProgress >= 0.5f && (state.currentComponent?.drawBackground ?: config.homeComponent.drawBackground) if (state.currentProgress >= 0.5f && (state.currentComponent?.drawBackground
|| state.currentProgress < 0.5f && config.homeComponent.drawBackground) { ?: config.homeComponent.drawBackground)
|| state.currentProgress < 0.5f && config.homeComponent.drawBackground
) {
8.dp.toPixels().toInt() 8.dp.toPixels().toInt()
} else { } else {
0 0
@ -1412,40 +1417,44 @@ internal fun LauncherScaffold(
bottom = filterBarHeight bottom = filterBarHeight
) )
config.homeComponent.Component( CompositionLocalProvider(
Modifier LocalScaffoldPage provides ScaffoldPage.Home,
.fillMaxSize() ) {
.combinedClickable( config.homeComponent.Component(
enabled = config.longPress != null || config.doubleTap != null, Modifier
onClick = {}, .fillMaxSize()
onLongClick = if (config.longPress != null) { .combinedClickable(
{ scope.launch { state.onLongPress() } } enabled = config.longPress != null || config.doubleTap != null,
} else null, onClick = {},
onDoubleClick = if (config.doubleTap != null) { onLongClick = if (config.longPress != null) {
{ scope.launch { state.onDoubleTap() } } { scope.launch { state.onLongPress() } }
} else null, } else null,
hapticFeedbackEnabled = false, onDoubleClick = if (config.doubleTap != null) {
indication = null, { scope.launch { state.onDoubleTap() } }
interactionSource = null, } else null,
) hapticFeedbackEnabled = false,
.homePageAnimation( indication = null,
state, interactionSource = null,
if (config.homeComponent.drawBackground) {
config.backgroundColor.copy(alpha = MaterialTheme.transparency.background)
} else {
Color.Transparent
}
),
insets = systemBarInsets
.let { if (config.homeComponent.hasIme) it.union(WindowInsets.ime) else it }
.let {
if (config.searchBarStyle == SearchBarStyle.Hidden) it else it.add(
searchBarInsets
) )
} .homePageAnimation(
.asPaddingValues(), state,
state if (config.homeComponent.drawBackground) {
) config.backgroundColor.copy(alpha = MaterialTheme.transparency.background)
} else {
Color.Transparent
}
),
insets = systemBarInsets
.let { if (config.homeComponent.hasIme) it.union(WindowInsets.ime) else it }
.let {
if (config.searchBarStyle == SearchBarStyle.Hidden) it else it.add(
searchBarInsets
)
}
.asPaddingValues(),
state
)
}
SecondaryPage( SecondaryPage(
state = state, state = state,
@ -1494,7 +1503,7 @@ internal fun LauncherScaffold(
highlightedAction = highlightedResult as? SearchAction, highlightedAction = highlightedResult as? SearchAction,
darkColors = config.darkSearchBar, darkColors = config.darkSearchBar,
isSearchOpen = state.currentComponent is SearchComponent && state.isSettledOnSecondaryPage || isSearchOpen = state.currentComponent is SearchComponent && state.isSettledOnSecondaryPage ||
config.homeComponent is SearchComponent && !state.isSettledOnSecondaryPage, config.homeComponent is SearchComponent && !state.isSettledOnSecondaryPage,
) )
} }
if (isFilterBarVisible) { if (isFilterBarVisible) {
@ -1566,6 +1575,7 @@ private fun SecondaryPage(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
insets: PaddingValues, insets: PaddingValues,
) { ) {
val components = remember(config) { val components = remember(config) {
setOfNotNull( setOfNotNull(
config.swipeUp?.component, config.swipeUp?.component,
@ -1602,7 +1612,11 @@ private fun SecondaryPage(
) )
val composable = composables[component] val composable = composables[component]
composable?.invoke(mod, insets, state) CompositionLocalProvider(
LocalScaffoldPage provides ScaffoldPage.Secondary
) {
composable?.invoke(mod, insets, state)
}
} }
// Keep other components alive, but out of the viewport // Keep other components alive, but out of the viewport
@ -1616,7 +1630,6 @@ private fun SecondaryPage(
v.invoke(modifier, insets, state) v.invoke(modifier, insets, state)
} }
} }
} }
private fun Modifier.homePageAnimation( private fun Modifier.homePageAnimation(
@ -1657,12 +1670,16 @@ private fun Modifier.homePageAnimation(
.background(backgroundColor) .background(backgroundColor)
} }
return this then component.homePageModifier(state, Modifier.background(backgroundColor).absoluteOffset { return this then component.homePageModifier(
IntOffset( state,
x = if (dir.orientation == Orientation.Horizontal) state.currentOffset.x.toInt() else 0, Modifier
y = if (dir.orientation == Orientation.Vertical) state.currentOffset.y.toInt() else 0 .background(backgroundColor)
) .absoluteOffset {
}) IntOffset(
x = if (dir.orientation == Orientation.Horizontal) state.currentOffset.x.toInt() else 0,
y = if (dir.orientation == Orientation.Vertical) state.currentOffset.y.toInt() else 0
)
})
} }
private fun Modifier.secondaryPageAnimation( private fun Modifier.secondaryPageAnimation(

View File

@ -0,0 +1,10 @@
package de.mm20.launcher2.ui.launcher.scaffold
import androidx.compose.runtime.compositionLocalOf
enum class ScaffoldPage {
Home,
Secondary,
}
val LocalScaffoldPage = compositionLocalOf<ScaffoldPage?> { null }

View File

@ -3,6 +3,7 @@ package de.mm20.launcher2.ui.launcher.transitions
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import com.android.launcher3.GestureNavContract import com.android.launcher3.GestureNavContract
import de.mm20.launcher2.ui.launcher.scaffold.LocalScaffoldPage
fun interface EnterHomeTransitionHandler { fun interface EnterHomeTransitionHandler {
fun handle(gestureNavContract: GestureNavContract): EnterHomeTransitionParams? fun handle(gestureNavContract: GestureNavContract): EnterHomeTransitionParams?
@ -11,11 +12,14 @@ fun interface EnterHomeTransitionHandler {
@Composable @Composable
fun HandleEnterHomeTransition(handler: EnterHomeTransitionHandler) { fun HandleEnterHomeTransition(handler: EnterHomeTransitionHandler) {
val transitionManager = LocalEnterHomeTransitionManager.current val transitionManager = LocalEnterHomeTransitionManager.current
DisposableEffect(handler) { val page = LocalScaffoldPage.current
transitionManager?.registerHandler(handler) if (page != null && transitionManager != null) {
DisposableEffect(handler, page) {
transitionManager.registerHandler(handler, page)
onDispose { onDispose {
transitionManager?.unregisterHandler(handler) transitionManager.unregisterHandler(handler, page)
}
} }
} }
} }

View File

@ -3,21 +3,24 @@ package de.mm20.launcher2.ui.launcher.transitions
import android.view.Window import android.view.Window
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.toAndroidRect import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.graphics.toAndroidRectF
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.IntSize
import androidx.core.graphics.toRectF import androidx.core.graphics.toRectF
import com.android.launcher3.GestureNavContract import com.android.launcher3.GestureNavContract
import de.mm20.launcher2.ui.launcher.scaffold.ScaffoldPage
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
class EnterHomeTransitionManager { class EnterHomeTransitionManager {
val currentTransition = MutableSharedFlow<EnterHomeTransition?>(1) val currentTransition = MutableSharedFlow<EnterHomeTransition?>(1)
private val handlers = mutableSetOf<EnterHomeTransitionHandler>() private val homeHandlers = mutableSetOf<EnterHomeTransitionHandler>()
private val secondaryHandlers = mutableSetOf<EnterHomeTransitionHandler>()
fun resolve(gestureNavContract: GestureNavContract, window: Window, page: ScaffoldPage) {
val handlers = if (page === ScaffoldPage.Secondary) secondaryHandlers else homeHandlers
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) {
@ -44,12 +47,23 @@ class EnterHomeTransitionManager {
currentTransition.tryEmit(null) currentTransition.tryEmit(null)
} }
fun registerHandler(handler: EnterHomeTransitionHandler) { /**
handlers.add(handler) * The scaffold page that needs to be active for this handler to be considered.
*/
fun registerHandler(handler: EnterHomeTransitionHandler, page: ScaffoldPage) {
if (page == ScaffoldPage.Home) {
homeHandlers.add(handler)
} else if (page == ScaffoldPage.Secondary) {
secondaryHandlers.add(handler)
}
} }
fun unregisterHandler(handler: EnterHomeTransitionHandler) { fun unregisterHandler(handler: EnterHomeTransitionHandler, page: ScaffoldPage) {
handlers.remove(handler) if (page == ScaffoldPage.Home) {
homeHandlers.remove(handler)
} else if (page == ScaffoldPage.Secondary) {
secondaryHandlers.remove(handler)
}
} }
} }